<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Mash-Up</title>
    <link>https://mash-up.tistory.com/</link>
    <description>성장의 즐거움을 아는 친구들, IT 개발 동아리 매쉬업 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 30 May 2026 11:06:46 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>MASHUP</managingEditor>
    <image>
      <title>Mash-Up</title>
      <url>https://tistory1.daumcdn.net/tistory/3888543/attach/d60246eeb40e44fa8c8af54c3d20ff1d</url>
      <link>https://mash-up.tistory.com</link>
    </image>
    <item>
      <title>[스프링팀] PK 숨기기</title>
      <link>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%8C%80-PK-%EC%88%A8%EA%B8%B0%EA%B8%B0</link>
      <description>&lt;p&gt;Mash-Up 스프링팀에서는 팀원들이 돌아가면서 본인의 Problem-Solving 경험이나 Deep Dive의 내용을 발표하는 자리를 갖습니다. 이번 14기 활동 때 발표한 내용중 하나인 &lt;strong&gt;&lt;code&gt;PK 숨기기&lt;/code&gt;&lt;/strong&gt; 에 대해 이번 포스팅에서 다뤄보겠습니다. 발표자는 프로젝트 개발 중 Front 개발자가 &lt;code&gt;우리의 PK를 과연 외부에 노출해도 괜찮을까?&lt;/code&gt; 라는 질문에 대해 논의를 하다가 이 문제를 해결했다는데요. 문제를 해결하기 위해 어떤 고민을 했고 어떤 해결방법을 도출해냈는지 함께 알아봅시다.&lt;/p&gt;
&lt;h1&gt;PK 숨기기&lt;/h1&gt;
&lt;h1&gt;개요&lt;/h1&gt;
&lt;p&gt;프로젝트 개발 중 무분별하게 노출되는 PK에 대해서 프론트 개발자와 &lt;code&gt;PK 과연 외부에 노출해도 괜찮은것일까?&lt;/code&gt; 에 대해 논의를 하다가 나중에 구현해보자! 라고 생각하다 문득 생각이 나서 이 문제를 해결해보게 되었다.&lt;/p&gt;
&lt;h1&gt;PK 과연 외부에 노출해도 괜찮을까?&lt;/h1&gt;
&lt;h2&gt;REST API 에서 pk를 외부에 노출하는 경우&lt;/h2&gt;
&lt;p&gt;요즘 개발을 한다고 하면 대부분이 REST 방식의 API를 구현하는 경우가 대부분이다. 그리고 리소스에 접근하는 방식이 대개 다음과 같은 방식으로 이루어진다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GET /api/v1/posts/{postId}&lt;/code&gt; , &lt;code&gt;GET /api/v1/comments/{commentId]&lt;/code&gt; 등 PK를 통해서 직접적으로 접근하는 경우가 대부분이다. 이 경우 PK가 AutoIncrement 형식의 Long 형 값이라면 공격자 입장에서 무분별하게 특정 리소스에 접근이 가능할 것이다. 만약 해당 리소스에 대해 특정 사용자만 접근이 가능하다고 할 때 서비스 내부적으로 특정 사용자가 아닌 경우에 대한 권한 처리를 제대로 하고 있다면 문제가 없을테지만 그렇지 않은 경우 원치 않은 사용자에게 리소스 정보가 넘어갈 수 있다.&lt;/p&gt;
&lt;p&gt;심지어 그것이 AutoIncrement 형식의 PK 라면 리소스가 만들어진 순서에 따라 순차적으로 리소스에 접근할 수 있게 된다. 즉, 추측이나 인젝션 공격의 대상이 될 수 있다. 예를 들어 userId라는 식별자를 생각해봤을 때 &lt;code&gt;GET /users/{userId}&lt;/code&gt; 요청으로 의도치 않게 타인의 마이페이지 접속 루트가 열리는 문제가 생길 수 있을 것이고. 특정 연속되는 값을 넣는다면 해당 서비스의 회원 수를 유추해 볼 수도 있다. &lt;/p&gt;
&lt;p&gt;실제로 2020년 5월 특정 서비스에서 유저 아이디를 URL PATH로 사용해 유저의 거래 내역을 노출하는 GET API에 대해 URL 숫자 조작만으로 다른 이의 거래 내역을 손쉽게 드러낸 경우도 있다.&lt;/p&gt;
&lt;p&gt;그렇다면 PK를 UUID로 변경하면 될까?&lt;/p&gt;
&lt;h2&gt;PK를 AutoIncrement가 아닌 UUID를 사용하는 경우&lt;/h2&gt;
&lt;p&gt;UUID를 사용하면 확실히 AutoIncrement PK에 비해 리소스에 무분별하게 접근할 수는 없을 것이다. 하지만 Auto Increment 보다 더 많은 공간을 차지하게 돼서 데이터베이스 인덱싱 및 스토리지에 오버헤드가 발생할 수 있다. 심지어 PK를 외부에 노출하지 않겠다고 모든 테이블 key 값을 UUID로 변경하는 것은 너무 과한 조치인 것 같다.&lt;/p&gt;
&lt;h2&gt;공통적인 문제&lt;/h2&gt;
&lt;p&gt;AutoIncrement PK, UUID PK 등 뭐든간에 PK를 외부에 노출하면 결국 데이터베이스의 구조와 공격자에게 도움이 될 만한 정보가 외부에 들어난다는 것이다. 그러면 UUID를 사용하지 않는 방법으로 이 문제를 해결해보자&lt;/p&gt;
&lt;h1&gt;요구사항&lt;/h1&gt;
&lt;p&gt;구현하기 앞서 다음과 같은 요구사항을 생각해보았다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이 문제를 해결하기 위해 다른 의존성이 생기지 않았으면 한다.&lt;/li&gt;
&lt;li&gt;최대한 핵심 로직을 건들지 않는 선에서 해결했으면 좋겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;해결 방법&lt;/h1&gt;
&lt;h2&gt;PK 매핑&lt;/h2&gt;
&lt;p&gt;내부적으로는 AutoIncrement PK를 사용하고 외부로 노출되는 PK는 UUID를 사용해 원본 PK(AutoIncrement)와 UUID를 매핑하는 테이블을 만드는 것이다. 하지만 RDB가 아닌 다른 NoSQL 저장소를 쓴다고 해도 Persistence 부분에 의존성이 생기기 때문에 이 해결방법은 제외했다.&lt;/p&gt;
&lt;h2&gt;PK 암호화&lt;/h2&gt;
&lt;p&gt;서비스에 들어오고 나가는 PK에 대해 모두 암호화를 시켜주는 방식이다. Controller 부분에서 요청을 받고 응답을 줄 때 암호화를 해주면 되기에 핵심로직(Service)에 무리가 가지 않으며 의존성이 추가되진 않기에 이 해결방법을 채택했다.&lt;/p&gt;
&lt;h1&gt;아이디어&lt;/h1&gt;
&lt;p&gt;위의 요구사항 2번에 핵심 로직을 건들지 않는 선에서 해결하기 원했기에 극한으로 핵심 로직을 건들지 않게 실제로 요청을 받고 처리하는 부분에서는 내부적으로 사용하는 PK값을 온전히 전달받을 수 있게 구현하고자 했다. 이를 위해 사용한 기술은 다음과 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ArgumentResolver&lt;ol&gt;
&lt;li&gt;ArgumentResolver를 사용해 Parameter로 해당 pk가 들어온 경우 이를 추출해 Long 형 타입으로 바꿔주기 위해 사용했다&lt;/li&gt;
&lt;li&gt;PathVariableArgumentResolver를 사용해 path에 pk를 담은 경우 이를 추출해 Long 형 타입으로 바꿔주기 위해 사용했다&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;AOP&lt;ol&gt;
&lt;li&gt;Response 조차 Long형 타입에 간단히 어노테이션을 붙이는 것 만으로 자동으로 String으로 변경해서 외부로 내보낼 수 있게 하기 위해 사용하였다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;ByteBuddy&lt;ol&gt;
&lt;li&gt;클래스를 동적으로 생성하기 위한 라이브러리이다. AOP를 통해 Response를 외부로 내보낼 때 암호화 할 PK의 타입을 변경해 새로운 클래스를 만들기 위해 사용하였다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;구현&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이 프로젝트는 Long 형 타입의 AutoIncrement PK를 사용하였다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;기본 서비스&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;SampleController&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@RequestMapping(&amp;quot;/api/v1/sample&amp;quot;)
@RestController
@SecretTarget
class SampleController(private val sampleService: SampleService) {

    @PostMapping
    fun create(@RequestBody request: SampleRequest): Response&amp;lt;Any&amp;gt; {
        return Response.success(sampleService.create(request.email, request.nickname))
    }

    @GetMapping(&amp;quot;/get&amp;quot;)
    fun get1(@SecretPk id: Long): Response&amp;lt;Any&amp;gt; {
        return Response.success(data = sampleService.getById(id))
    }

    @GetMapping(&amp;quot;/get/{id}&amp;quot;)
    fun get2(@SecretPkPathVariable(&amp;quot;id&amp;quot;) id: Long): Response&amp;lt;Any&amp;gt; {
        return Response.success(data = sampleService.getById(id))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;SampleService&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Service
class SampleService {

    private val userMap = ConcurrentHashMap&amp;lt;Long, SampleResponse&amp;gt;()

    fun create(email: String, nickname: String): SampleResponse {
        val nextId = (userMap.map { it.key }.maxOrNull() ?: -1) + 1
        userMap[nextId] = SampleResponse(nextId, email, nickname)
        return userMap[nextId]!!
    }

    fun getById(id: Long): SampleResponse {
        return userMap[id] ?: throw IllegalArgumentException(&amp;quot;cannot find user, userId: $id&amp;quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inmemeory 캐싱을 통해 SampleResponse를 주고 받는 간단한 서비스다.&lt;/p&gt;
&lt;h2&gt;Encryptor&lt;/h2&gt;
&lt;p&gt;실제로 PK를 암/복호화 해주는 Encryptor를 만들어보자&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class SecretEncryptor(private val property: SecretPkProperty) {

    private val secretKey: SecretKey

    init {
        val keyBytes = property.secretKey.toByteArray()
        secretKey = SecretKeySpec(keyBytes, property.algorithm)
    }

    fun encryptLongToString(value: Long): String {
        val cipher = Cipher.getInstance(property.algorithm)
        cipher.init(Cipher.ENCRYPT_MODE, secretKey)

        val byteBuffer = ByteBuffer.allocate(8)
        byteBuffer.putLong(value)
        val longBytes = byteBuffer.array()
        val encryptedBytes = cipher.doFinal(longBytes)
        return Base64.getEncoder().encodeToString(encryptedBytes)
    }

    fun decryptStringToLong(encryptedString: String): Long {
        val cipher = Cipher.getInstance(property.algorithm)
        cipher.init(Cipher.DECRYPT_MODE, secretKey)

        val encryptedBytes = Base64.getDecoder().decode(encryptedString)
        val decryptedBytes = cipher.doFinal(encryptedBytes)
        return ByteBuffer.wrap(decryptedBytes).getLong()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;javax.crypto 를 사용해서 SecretPkProperty(algorithm, key) 를 입력받아 SecretKey를 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SecretPkConfig&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Configuration
@EnableConfigurationProperties(SecretPkProperty::class)
class SecretPkConfig {
    @Bean
    fun secretEncryptor(property: SecretPkProperty): SecretEncryptor {
        return SecretEncryptor(property)
    }
}
@ConfigurationProperties(prefix = &amp;quot;secret.pk&amp;quot;)
data class SecretPkProperty(val algorithm: String, val secretKey: String)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ArgumentResolver&lt;/h2&gt;
&lt;p&gt;이제 실제로 path나 parameter로 들어온 id 값을 추출해 복호화 한 후 Controller에 던져주자.&lt;/p&gt;
&lt;p&gt;기본적으로 들어오는 형식은 다음과 같을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/api/v1/sample/{sampleId}&lt;/code&gt; or &lt;code&gt;/api/v1/sample?id={sampleId}&lt;/code&gt; &lt;/p&gt;
&lt;p&gt;Parameter로 들어온 경우&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SecretPkArgumentResolver&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class SecretPkArgumentResolver(private val encryptor: SecretEncryptor) : HandlerMethodArgumentResolver {

    private val log: Logger = LoggerFactory.getLogger(SecretPkArgumentResolver::class.java)

    override fun supportsParameter(parameter: MethodParameter): Boolean {
        return parameter.hasParameterAnnotation(SecretPk::class.java)
                &amp;amp;&amp;amp; parameter.parameter.type == Long::class.java
    }

    override fun resolveArgument(
        parameter: MethodParameter,
        mavContainer: ModelAndViewContainer?,
        webRequest: NativeWebRequest,
        binderFactory: WebDataBinderFactory?
    ): Any {
        val param = webRequest.parameterMap[parameter.parameterName]?.firstOrNull()
        log.info(&amp;quot;parameter.parameter: ${parameter.parameter}, parameter: $parameter, param: $param&amp;quot;)
        param ?: throw IllegalStateException(&amp;quot;param must not be null&amp;quot;)
        return encryptor.decryptStringToLong(param) // param을 입력받아 decrypt 작업 수행
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Path로 들어온 경우&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SecretPkPathVariableArgumentResolver&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class SecretPkPathVariableArgumentResolver(private val encryptor: SecretEncryptor) : HandlerMethodArgumentResolver {

    override fun supportsParameter(parameter: MethodParameter): Boolean {
        return parameter.hasParameterAnnotation(SecretPkPathVariable::class.java)
    }

    override fun resolveArgument(
        parameter: MethodParameter,
        mavContainer: ModelAndViewContainer?,
        webRequest: NativeWebRequest,
        binderFactory: WebDataBinderFactory?
    ): Any {
        val pathVariables = webRequest.getAttribute(
            HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
            NativeWebRequest.SCOPE_REQUEST
        ) as Map&amp;lt;*, *&amp;gt;

        val paramName = parameter.parameterName ?: throw IllegalArgumentException(&amp;quot;Parameter name is null&amp;quot;)
        val param = pathVariables[paramName] as? String ?: throw IllegalArgumentException(&amp;quot;Parameter name is null&amp;quot;)
        return encryptor.decryptStringToLong(param)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;실제 사용법&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@GetMapping(&amp;quot;/get&amp;quot;)
fun get1(@SecretPk id: Long): Response&amp;lt;Any&amp;gt; {
    return Response.success(data = sampleService.getById(id))
}

@GetMapping(&amp;quot;/get/{id}&amp;quot;)
fun get2(@SecretPkPathVariable(&amp;quot;id&amp;quot;) id: Long): Response&amp;lt;Any&amp;gt; {
    return Response.success(data = sampleService.getById(id))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@SecretPk&lt;/code&gt; 어노테이션을 사용하면 SecretPkArgumentResolver를 통해 파라미터에 담긴 id를 decryp 해서 Long 형 타입으로 PK를 바로 받아올 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@SecretPkPathVariable&lt;/code&gt; 어노테이션을 사용해 SecretPkPathArgumentResolver를 통해 path에 담긴 id를 복호화해서 바로 받아올 수 있다.&lt;/p&gt;
&lt;p&gt;이를 통해 사용자는 id가 String이었는지 암호화가 어떻게 수행되는지 전혀 알 필요 없이 핵심 로직에만 집중할 수 있다.&lt;/p&gt;
&lt;h2&gt;SecretPkAspect&lt;/h2&gt;
&lt;p&gt;그러면 암호화는 어떻게 진행할까? 사실 가장 심플한 방법은 DTO에 String 타입의 id를 적어주면 된다. 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class SampleResponse(
    val id: String,
    val email: String,
    val nickname: String
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 이 또한 Response에 정보를 담으면서 id를 암호화 해줘야 하는 필요성이 있다. 핵심 로직에 집중하기 위해 id를 다음과 같이 변경해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class SampleResponse(
    @field:SecretPk val id: Long,
    val email: String,
    val nickname: String
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 SecretPk가 달린 필드의 경우 AOP에서 알아서 String 형태로 변환한 후 암호화 해서 내보냈으면 하는 요구사항이 있었다. 어떻게 해결할 수 있을까?&lt;/p&gt;
&lt;h3&gt;Filter를 사용해보자&lt;/h3&gt;
&lt;p&gt;Filter는 Interceptor와는 다르게 Response 조작이 가능하다. 하지만 이는 Spring MVC 외부에 있기에 클래스 원형에 접근하기 어렵다. 왜냐하면 우리는 &lt;code&gt;@SecretPk&lt;/code&gt; 가 붙은 함수 원형을 참조해야 하기 때문에 반환 타입의 정보가 필요하기 때문이다!&lt;/p&gt;
&lt;h3&gt;Spring AOP를 사용해보자&lt;/h3&gt;
&lt;p&gt;이 또한 작은 문제점이 존재한다. Controller 단의 반환 값을 변경하는 것이기 때문에 반환값은 결국 동일해야 한다. id를 Long → String으로 변경한다면 상속도 불가능하니 새로운 클래스가 필요한데 클래스를 정적이든 동적이든 변경한다면 반환 타입의 불일치의 문제가 생긴다. 이를 Wrapper를 둬서 어느정도 합의를 보았다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;fun get1(@SecretPk id: Long): Response&amp;lt;Any&amp;gt; {
    return Response.success(data = sampleService.getById(id))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Response&lt;Any&gt;를 반환해 안에 담길 data 타입이 변경 가능하도록 설정해줘야 했다. (가장 꼬롬함)&lt;/p&gt;
&lt;p&gt;반환값을 단순히 String으로 변환해서 내려줄까? (objectMapper 이용해서) 아니면 동적으로 클래스를 새로 생성해서 반환할까? 고민을 많이 하다가 후자의 방식을 선택해 reflection 기술과 ByteBuddy 라는 라이브러리를 사용해 구현했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SecretPkAspect&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;var dynamicClassBuilder = byteBuddy
      .subclass(Any::class.java)
      .name(resultDataClassName + SUFFIX_DYNAMIC_CLASS_NAME)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;우선 ByteBuddy를 통해 빈 껍데기 클래스를 만들어주자. 클래스 이름은 원본 클래스명 + _S(Secret)을 붙였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;memberProperties.forEach { property -&amp;gt;
  val hasSecretPkAnnotation =
      property.javaField?.annotations?.firstOrNull()?.annotationClass == SecretPk::class
  dynamicClassBuilder = if (hasSecretPkAnnotation) {
      dynamicClassBuilder.defineField(property.name, String::class.java, Visibility.PUBLIC)
  } else {
      dynamicClassBuilder.defineField(
          property.name,
          property.returnType.jvmErasure.java,
          Visibility.PUBLIC
      )
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;property를 돌면서 SecretPk 어노테이션이 붙은 필드의 경우 String으로 나머지는 본래 자신의 클래스를 따라가게 변경했다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;property.returnType.jvmErasure.java&lt;/code&gt; 를 통해서 자바 클래스에 접근해야 한다. &lt;code&gt;property.javaClass&lt;/code&gt; 는 자바 클래스가 아니다 KPropery1을 포함한 자바 클래스다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val newClass = dynamicClassBuilder
    .make()
    .load(SecretPkAspect::class.java.classLoader)
    .loaded&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;클래스 정의를 다 하고 나면 부모 객체의 ClassLoader를 통해 클래스를 로드 시켜줘야한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;make&lt;/code&gt; 함수를 통해 바이트 코드를 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val newClassInstance = newClass.getDeclaredConstructor().newInstance()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만들어진 클래스를 바탕으로 새로운 인스턴스를 만든 후&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;memberProperties.forEach { property -&amp;gt;
    val hasSecretPkAnnotation =
        property.javaField?.annotations?.firstOrNull()?.annotationClass == SecretPk::class
    if (hasSecretPkAnnotation) {
        val encrypted = (getPropertyValue(data, property.name) as? Long)
            ?.let { encryptor.encryptLongToString(it) }
            ?: throw IllegalStateException(&amp;quot;@SecretPk fields must be Long&amp;quot;)
        newClass.getField(property.name).set(newClassInstance, encrypted)
    } else {
        val value = getPropertyValue(data, property.name)
        newClass.getField(property.name).set(newClassInstance, value)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다시 프로퍼티를 돌면서 실제 값을 넣어주자! &lt;code&gt;SecretPk&lt;/code&gt; 어노테이션이 붙어있는 경우는 &lt;code&gt;encryptLongToString&lt;/code&gt;을 통해 Long형 PK를 암호화 해서 넣어주고 아닌 경우는 실제 값을 그대로 넣어준다.&lt;/p&gt;
&lt;h2&gt;SecretPkAspect 사용법&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@RequestMapping(&amp;quot;/api/v1/sample&amp;quot;)
@RestController
@SecretTarget
class SampleController(private val sampleService: SampleService) {

    @PostMapping
    fun create(@RequestBody request: SampleRequest): Response&amp;lt;Any&amp;gt; {
        return Response.success(sampleService.create(request.email, request.nickname))
    }

    @GetMapping(&amp;quot;/get&amp;quot;)
    fun get1(@SecretPk id: Long): Response&amp;lt;Any&amp;gt; {
        return Response.success(data = sampleService.getById(id))
    }

    @GetMapping(&amp;quot;/get/{id}&amp;quot;)
    fun get2(@SecretPkPathVariable(&amp;quot;id&amp;quot;) id: Long): Response&amp;lt;Any&amp;gt; {
        return Response.success(data = sampleService.getById(id))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@SecretTarget&lt;/code&gt; 을 통해 JoinPoint를 설정해주고 암호화할 요청은 &lt;code&gt;@SecretPk&lt;/code&gt; or &lt;code&gt;@SecretPkPathVariable&lt;/code&gt; 를 통해 설정해주고 응답의 경우&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class SampleResponse(
    @field:SecretPk val id: Long,
    val email: String,
    val nickname: String
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;data class field 단에 &lt;code&gt;SecretPk&lt;/code&gt; 어노테이션을 달아주면 알아서 AOP가 동작한다!&lt;/p&gt;
&lt;h1&gt;결과&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;create&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UoLmr/btsKa7mnH1N/c7A8rHPcH0zDBXb5VS66I0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UoLmr/btsKa7mnH1N/c7A8rHPcH0zDBXb5VS66I0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UoLmr/btsKa7mnH1N/c7A8rHPcH0zDBXb5VS66I0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUoLmr%2FbtsKa7mnH1N%2Fc7A8rHPcH0zDBXb5VS66I0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;get(Path)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  &lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yF81z/btsJ9On4dSi/OcBdkAyNqB4EIxpkEA6EuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yF81z/btsJ9On4dSi/OcBdkAyNqB4EIxpkEA6EuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yF81z/btsJ9On4dSi/OcBdkAyNqB4EIxpkEA6EuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyF81z%2FbtsJ9On4dSi%2FOcBdkAyNqB4EIxpkEA6EuK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;get(Parameter)&lt;/strong&gt;&lt;br&gt;  &lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t6iJs/btsKa5hMCjY/CkjiKi2kAwbuaKleE1Lfl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t6iJs/btsKa5hMCjY/CkjiKi2kAwbuaKleE1Lfl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t6iJs/btsKa5hMCjY/CkjiKi2kAwbuaKleE1Lfl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft6iJs%2FbtsKa5hMCjY%2FCkjiKi2kAwbuaKleE1Lfl1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>팀 이야기</category>
      <category>AOP</category>
      <category>secret pk</category>
      <category>Spring</category>
      <category>spring boot</category>
      <author>MASHUP</author>
      <guid isPermaLink="true">https://mash-up.tistory.com/83</guid>
      <comments>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%8C%80-PK-%EC%88%A8%EA%B8%B0%EA%B8%B0#entry83comment</comments>
      <pubDate>Fri, 18 Oct 2024 00:58:46 +0900</pubDate>
    </item>
    <item>
      <title>[스프링팀] Coroutine 찍어먹기</title>
      <link>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%8C%80-Coroutine-%EC%B0%8D%EC%96%B4%EB%A8%B9%EA%B8%B0</link>
      <description>&lt;p&gt;요즘 비동기 프로그래밍에 대한 논의는 앱이나 웹과 같은 프론트 사이드의 진영뿐 아니라 서버 진영에서도 심심치 않게 들리는 화두가 되었습니다. 일례로 Java 21 버전에서 소개된 Virtual Thread와 같이 최신 기술 트렌드에 비동기 프로그래밍은 항상 뜨거운 감자와도 같은 주제입니다. 이번 포스팅에서는 스프링 팀에서 비동기 프로그래밍 방법 중 신흥 강자로 소개되고 있는 Kotlin Corouine이 왜 경량 스레드라고 소개되고 있는지 어떠한 컨셉을 갖고 있는지에 대해 다뤄보고자 합니다.&lt;/p&gt;
&lt;h1&gt;Why Coroutine?&lt;/h1&gt;
&lt;h2&gt;비동기 프로그래밍이란?&lt;/h2&gt;
&lt;h3&gt;동기(Synchronous)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;현재 작업의 응답이 끝남과 동시에 다음 작업이 요청된다.&lt;/li&gt;
&lt;li&gt;함수를 호출하는 곳에서 호출되는 함수가 결과를 반환할 때까지 기다린다.&lt;/li&gt;
&lt;li&gt;작업 완료 여부를 계속해서 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;비동기(Asynchronous)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;현재 작업의 응답이 끝나지 않은 상태에서 다음 작업이 요청된다.&lt;/li&gt;
&lt;li&gt;함수를 호출하는 곳에서 결과를 기다리지 않고, 다른 함수(callback)에서 결과를 기다린다.&lt;/li&gt;
&lt;li&gt;작업 완료 여부를 확인하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;비동기 프로그래밍 방식 소개&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;연속적인 두 개의 함수를 처리하는 코드를 각기 다른 비동기 프로그래밍 스타일로 작성해보자.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;Callback&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface Callback {
    fun onSuccess()
    fun onFailure()
}

private val dispatcher: ExecutorService = Executors
.newCachedThreadPool { runnable -&amp;gt;
    Thread(runnable).apply {
          isDaemon = true
    }
}

fun doHardWork(callback: Callback): Disposable {
    // .. do Hard Work ..
    return dispatcher.submit {
        if(isSuccess) {
            callback.onSuccess()
        } else {
            callback.onFailure()
        }
    }.asDisposable()
}

fun doNextWork(callback: Callback): Disposable {
        return dispatcher.submit {
            if(isSuccess) {
                callback.onSuccess()
            } else {
                callback.onFailure()
            }
        }.asDisposable()
}

fun main() {
    doHardWork( object: Callback {
            override fun onSuccess() {
                ...
                doNextWork(object: Callback {
                    override fun onSuccess() {
                        ...
                    }

                    override fun onFailure() {
                        ...
                    }
                }
            }

            override fun onFailure() {
                // print error log
            }
        }
    )
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Rx&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fun doHardWork(): Single&amp;lt;Boolean&amp;gt; =    Single.create { emitter -&amp;gt; 
        // .. do hard thing ..
        emitter.onSuccess(isSuccess)
}.subscribeOn(Schedulers.IO)
fun doNextWork(): Single&amp;lt;Boolean&amp;gt; = Single.create { emitter -&amp;gt; 
        // ..do next work ..
        emitter.onSuccess(isSuccess)
}.subscribeOn(Schedulers.IO)

fun main() {
    doHardWork()
        .flatMap { isSuccess -&amp;gt; 
            if(isSuccess) {
                doNextWork()
            } else {
                Single.error(&amp;quot;Failed to do hard wrok&amp;quot;)
            }
        .doOnError { // Print error log }
        .observeOn(Schedulers.MAIN)
        .subscribe { isSuccess -&amp;gt;
            println(&amp;quot;is it success? $isSuccess&amp;quot;)
        }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;&lt;strong&gt;Callback 방식과 Rx 방식 비교&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Callback 방식의 경우 연쇄적인 함수를 호출할 경우 Callback 지옥에 빠질 수 있다&lt;/li&gt;
&lt;li&gt;Rx 방식에서는 비동기 작업의 수행 스레드와 결과 처리 스레드 지정을 손쉽게 설정할 수 있다&lt;/li&gt;
&lt;li&gt;Rx 방식에서는 작업 간의 관계 및 스레드 지정이 Chaining 방식으로 작성 가능하여 가독성이 더 좋아진다. (다만, Rx가 제공하는 일련의 Operator를 능숙하게 사용하는 경우에만 가독성의 장점을 챙길 수 있다.)&lt;ul&gt;
&lt;li&gt;exampledata 제어 : map, filter, distinct ..&lt;/li&gt;
&lt;li&gt;event 처리 : doOnError, doOnSubscribe, doOnSuccess, doOnDispose ..&lt;/li&gt;
&lt;li&gt;stream 변경 : flatMap, concatMap, switchMap, compose, switchIfEmpty, …&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;또한 체인에 예외나 취소에 대한 핸들링이 용이하다&lt;/li&gt;
&lt;li&gt;But, Rx 체인의 디버깅은 아직 어렵다. doOn… 등의 operator를 사용해서 디버깅 하는 수준&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Coroutine&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;suspend fun doHardWork(): Boolean {
    // .. do hard work ..
    return isSuccess
}
suspend fun doNextWork(): Boolean {
    return isSuccess
}

fun main() = runBlocking {
    launch(Dispatchers.IO) {
        if(doHardWork()) {
            doNextWork()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;&lt;strong&gt;Callback 및 Rx 방식과 코루틴 방식 비교&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;코루틴 역시 Rx 처럼 작업 수행 스레드와 결과 처리 스레드를 지정하기 용이하다&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;hr&gt;
&lt;h1&gt;Coroutine 기본 이해&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) {
    GlobalScope.launch {
        delay(1000L)
        println(&amp;quot;World!&amp;quot;)
    }
    println(&amp;quot;Hello,&amp;quot;)
    Thread.sleep(2000L) // for blocking main thread
}&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;Hello,
World!&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;delay() 함수는 코루틴 내에서 사용할 수 있는 중단 함수. 즉, GlobalScope.launch {} 는 코루틴 빌더이다.&lt;/li&gt;
&lt;li&gt;Thread.sleep(2000L) 를 통해 MainThread를 2초간 Block 하여, 메인함수의 종료를 지연시킨다. 좀 더 자연스러운 스타일로 바꿔보자&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) = runBlocking {
    GlobalScope.launch {
        delay(1000L)
        println(&amp;quot;World!&amp;quot;)
    }
    println(&amp;quot;Hello,&amp;quot;)
    delay(2000L)
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;runBlocking{} 블록은 주어진 블록이 완료될 때까지 현재 스레드를 멈추는 코루틴 빌더이다&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;  이렇게 임의의 시간 (2초) 를 가지고 메인 스레드를 Block하는 것은 좋은 로직이 아니다. 부모 코루틴(runBlocking block = main function)은 자식 코루틴 (GlobalScope.launch{}) 의 작업이 완료되고 종료되는데 까지 얼마나 걸리는지 예측할 수 없기 때문이다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) = runBlocking {
    val job = GlobalScope.launch {
        delay(1000L)
        println(&amp;quot;World!&amp;quot;)
    }
    println(&amp;quot;Hello,&amp;quot;)
    job.join()
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt; 이 또한, 모든 자식 코루틴이 실행될 때마다 Job 객체를 참조하여 join을 호출하는 방식은 매우 불편하다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;모든 코루틴들은 각자의 스코프를 갖는다. 부모 코루틴과 자식 코루틴이 같은 코루틴 스코프 내에 위치하면 부모 코루틴은 알아서 자식 코루틴이 종료되길 기다린다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) = runBlocking { // this -&amp;gt; runBlock&amp;#39;s coroutineScope
    launch {
        delay(1000L)
        println(&amp;quot;World!&amp;quot;)
    }
    println(&amp;quot;Hello,&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;launch 함수는 CoroutineScope.launch() 로 CoroutineScope의 확장 함수이다. runBlocking 블록에서 this 참조로 runBlocking의 coroutineScope에 접근이 가능하며, 위의 코드는 부모 코루틴과 자식 코루틴의 코루틴 스코프가 일치한 모습이다.&lt;/p&gt;
&lt;h3&gt;CoroutineScope&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) = runBlocking {
    launch {
        delay(200L)
        println(&amp;quot;1&amp;quot;)
    }

    coroutineScope {
        launch {
            delay(500L)
            println(&amp;quot;2&amp;quot;)
        }
        delay(100L)
        println(&amp;quot;3&amp;quot;)
    }
    println(&amp;quot;4&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;실행결과runBlocking과 coroutineScope의 차이는 runBlocking은 자식 코루틴이 종료되길 기다리면서 현재 스레드를 블락하고 coroutineScope은 그렇지 않다는 차이가 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3 1 2 4&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;중단 함수&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) = runBlocking {
    val job = launch {
        doWorld()
    }
        val deferred = async {
            doWorld2()
        }
        deferred.await()
    println(&amp;quot;Hello,&amp;quot;)
}

suspend fun doWorld() {
    delay(1000L)
    println(&amp;quot;World!&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;가벼움&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) = runBlocking {
    repeat(100_000) {
        launch {
            delay(1000L)
            print(&amp;quot;.&amp;quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Global 코루틴은 deamon thread 처럼 동작한다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) = runBlocking {
    GlobalScope.launch {
        repeat(1000) { i -&amp;gt;
            println(&amp;quot;I&amp;#39;m sleeping $i ...&amp;quot;)
            delay(500L)
        }
    }
    delay(1300L)
}&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;I’m sleeping 0 …
I’m sleeping 1 …
I’m sleeping 2 …&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Coroutine Context란?&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public interface CoroutineContext {
  public operator fun &amp;lt;E : Element&amp;gt; get(key: Key&amp;lt;E&amp;gt;): E?
  public fun &amp;lt;R&amp;gt; fold(initial: R, operation: (R, Element) -&amp;gt; R): R
  public operator fun plus(context: CoroutineContext): CoroutineContext = ...impl...
  public fun minusKey(key: Key&amp;lt;*&amp;gt;): CoroutineContext
}
public interface Key&amp;lt;E : Element&amp;gt;
public interface Element : CoroutineContext {
  public val key: Key&amp;lt;*&amp;gt;
  ...overrides...
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;get() : 연산자 함수로써 주어진 key 에 해당하는 컨텍스트 요소를 반환한다&lt;/li&gt;
&lt;li&gt;fold() : 초기값을 시작으로 제공된 병합 함수를 이용하여 대상 컨텍스트 요소들을 병합한 후 결과를 반환한다&lt;/li&gt;
&lt;li&gt;plus() : 현재 컨텍스트와 파라미터로 주어진 다른 컨텍스트가 갖는 요소들을 모두 포함하는 컨텍스트를 반환한다. 중복을 포함하지 않는다&lt;/li&gt;
&lt;li&gt;minusKey() : 현재 컨텍스트에서 주어진 키를 갖는 요소들을 제외한 새로운 컨텍스트를 반환한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Element 는 CoroutineContext 를 상속하며 key 를 멤버 속성으로 갖는다. CoroutineContext 를 구성하는 Element 들의 예를 들어보면 CoroutineId, CoroutineName, CoroutineDispatcher, ContinuationInterceptor, CoroutineExceptionHandler 등이 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;  즉, 코루틴 컨텍스트에는 코루틴 컨텍스트를 상속한 Element 들이 등록될 수 있고, 각 Element들이 등록 될때는 Element의 고유한 Key를 기반으로 등록된다는 것&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;launch() {} // Only CoroutineId

// CoroutineId + Continuation Interceptor
launch(Dispatchers.IO) {}

// CoroutineId + CoroutineName + Continuation Interceptor
launch(Dispatchers.IO + CoroutineName(&amp;quot;SampleCoroutine&amp;quot;)){} 

// CoroutineId + CoroutineName + Coroutine ExcpetionHandler + Continuation Interceptor
launch(Dispatchers.IO + CoroutineName(&amp;quot;SampleCoroutine&amp;quot;) + CoroutineExceptionHandler()){} 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;‘+’ 연산자를 이용해 Element 들 (CoroutineContext) 를 병합해 CombinedContext를 만들어 낸다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;  즉, CoroutineContext는 코루틴의 실행환경에 대한 정보라고 생각하면 쉽다!&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;CoroutineScope이란?&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;CoroutineScope은 단순히 CoroutineContext를 갖고 있을 뿐이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -&amp;gt; Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;우리가 위에서 살펴본 launch 와 같은 코루틴 빌더들은 CoroutineScope의 확장함수이며, launch 블록으로 생성된 코루틴들은 CoroutineScope의 CoroutineContext 환경을 그대로 상속한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ExperimentalCoroutinesApi
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    val combined = coroutineContext.foldCopiesForChildCoroutine() + context
    val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
    return if (combined !== Dispatchers.Default &amp;amp;&amp;amp; combined[ContinuationInterceptor] == null)
        debug + Dispatchers.Default else debug
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;combined를 자세히 보면 현재 CoroutineScope의 coroutineContext와 새로 입력받은 context를 병합한 모습. default 로 EmptyCoroutineContext가 지정되어있는데 이건 뭘까?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public object EmptyCoroutineContext : CoroutineContext, Serializable {
    private const val serialVersionUID: Long = 0
    private fun readResolve(): Any = EmptyCoroutineContext

    public override fun &amp;lt;E : Element&amp;gt; get(key: Key&amp;lt;E&amp;gt;): E? = null
    public override fun &amp;lt;R&amp;gt; fold(initial: R, operation: (R, Element) -&amp;gt; R): R = initial
    public override fun plus(context: CoroutineContext): CoroutineContext = context
    public override fun minusKey(key: Key&amp;lt;*&amp;gt;): CoroutineContext = this
    public override fun hashCode(): Int = 0
    public override fun toString(): String = &amp;quot;EmptyCoroutineContext&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 코드를 보면 newCoroutineContext에서 ‘+’ 연산자로 &lt;code&gt;coroutineContext.foldCopiesForChildCoroutine() + context&lt;/code&gt;를 해봐도 EmptyCoroutineContext는 무시됨을 알 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;우리가 사용한 GlobalScope은 Singleton object로 coroutineContext로 EmptyCoroutineContext를 가지고 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;왜 Coroutine을 경량화된 Thread라고 할까?&lt;/h1&gt;
&lt;h3&gt;Coroutine은 Thread가 아니다&lt;/h3&gt;
&lt;p&gt;코루틴이 하나 생성된다고 해서 Thread가 하나 생성되는 것이 아니다. Coroutine은 스케쥴링 가능한 코드 블록 혹은 코드 블록의 집합이다.&lt;/p&gt;
&lt;p&gt;launch { } 와 같이 코루틴 빌더를 실행했을 경우 넘긴 코드 블록은 Continuation이라는 단위로 만들어진다. 여기서 말하는 Continuation은 &lt;a href=&quot;https://en.wikipedia.org/wiki/Continuation-passing_style&quot;&gt;CPS&lt;/a&gt;(Continuation Passing Style)에서 이야기하는 Continuation 개념의 구현체이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;em&gt;어떤 일을 수행하기 위한 일련의 함수들의 연결을 각 함수의 반환값을 이용하지 않고 Continuation 이라는 추가 파라미터(Callback)를 두어 연결하는 방식으로 Continuation 단위로 dispatcher 를 변경한다거나 실행을 유예한다거나 하는 플로우 컨트롤이 용이해지는 이점이 있다.&lt;/em&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Continuation 단위로 변경이 된 코드는 처음, suspended 상태로 대기하고 있다가 resume() 요청으로 resumed 상태로 전환되어 실행되고 추가적인 중단 함수를 만나면 suspended 상태로 전환되었다가 하는 등 suspended, resumed 상태를 번갈아가며 코드가 진행된다.&lt;/p&gt;
&lt;p&gt;이 때 resume() 요청을 할 때마다 dispatcher에게 dispatch(스레드 전환)이 필요한지 묻는 isDispatchNeeded() 함수를 통해 확인 후 필요한 경우 적합한 스레드로 전달되어 실행된다.&lt;/p&gt;
&lt;p&gt;즉, 스레드 전환이 필요하지 않으면 전환하지 않는다!!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) = runBlocking {
    repeat(100_000) {
        launch(Dispatchers.IO) {
            delay(1000L)
            print(&amp;quot;.&amp;quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위에서 본 이 코드에서 OOM이 발생하지 않는 이유이다! launch 빌더에서 Dispatcher를 재정의 하지 않았기 때문에 runBlocking의 dispatcher를 함께 사용한다. runBlocking 빌더는 내부적으로 GlobalScope을 사용하며 Dispatcher 는 BlockingEventLoop 을 사용해 이벤트 루프 기반으로 10만번의 이벤트를 발생하여 점을 출력하게 되며 스레드 부하는 없으므로 OOM을 피할 수 있게 된다.&lt;/p&gt;</description>
      <category>팀 이야기</category>
      <category>Coroutine</category>
      <category>kotlin coroutine</category>
      <category>Spring</category>
      <category>spring boot</category>
      <author>MASHUP</author>
      <guid isPermaLink="true">https://mash-up.tistory.com/82</guid>
      <comments>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%8C%80-Coroutine-%EC%B0%8D%EC%96%B4%EB%A8%B9%EA%B8%B0#entry82comment</comments>
      <pubDate>Fri, 18 Oct 2024 00:44:03 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] SpringBoot이 ObjectMapper를 구성하는 방법</title>
      <link>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81-SpringBoot%EC%9D%B4-ObjectMapper%EB%A5%BC-%EA%B5%AC%EC%84%B1%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 프로젝트에서는 객체를 Serialize 및 Deserialize 하는 동작들이 프레임워크 내부 동작에서 많이 활용되어지고 있습니다. Spring 을 어느정도 활용해 본 사람이라면 알고 있을 &lt;code&gt;ObjectMapper&lt;/code&gt; 를 사용해 객체의 Serialization및 Deserialization을 수행하고 있는데요. 여러 곳에서 사용하다보니 임의로 &lt;code&gt;ObjectMapper&lt;/code&gt; 및 &lt;code&gt;ObjectMapperBuilder&lt;/code&gt; 를 재정의 하는 경우를 종종 볼 수 있습니다. 이러한 방식으로 &lt;b&gt;이 객체들을 재정의 해도 문제가 없을지&lt;/b&gt;, &lt;b&gt;문제가 있다면 어떠한 문제가 있을 수 있을지&lt;/b&gt; 구현 코드를 보고 해당 객체들이 어떤 방식으로 구성되는지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  아래에 작성하는 코드는 &lt;a href=&quot;https://github.com/mkSpace/ObjectMapperLearning&quot;&gt;ObjectMapperLearning&lt;/a&gt; 에서 확인 가능합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ObjectMapper 주입 관계 도식화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 Spring Framework 내에서 사용하는 &lt;code&gt;ObjectMapper&lt;/code&gt; 는 여러 설정 객체의 정보들을 내려받아 최종적으로 &lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt;의 &lt;code&gt;build()&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;844&quot; data-origin-height=&quot;517&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T3jV1/btsJ7nXzh5z/kU4JMpx60BoQIQwRV5dXf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T3jV1/btsJ7nXzh5z/kU4JMpx60BoQIQwRV5dXf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T3jV1/btsJ7nXzh5z/kU4JMpx60BoQIQwRV5dXf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT3jV1%2FbtsJ7nXzh5z%2FkU4JMpx60BoQIQwRV5dXf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;844&quot; height=&quot;517&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;517&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ObjectMapper의 구성 정보를 담은 Builder 객체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;code&gt;ObjectMapper&lt;/code&gt;가 어떤 코드에서 주입 되었는지 확인해보면 아래와 같은 코드를 만날 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
  return builder.createXmlMapper(false).build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 &lt;b&gt;JacksonAutoConfiguration.java&lt;/b&gt;의 코드이며, &lt;code&gt;JacksonAutoConfiguration&lt;/code&gt;의 &lt;code&gt;JacksonObjectMapperConfiguration&lt;/code&gt; 이라는 static으로 생성된 Configuration 객체가 &lt;code&gt;ObjectMapper&lt;/code&gt;를 &lt;code&gt;@Bean&lt;/code&gt;을 통해 주입하고 있습니다. 또한 이 메서드는 &lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt; 를 주입 받아 &lt;code&gt;ObjectMapper&lt;/code&gt;를 생성하고 있습니다. 그럼 &lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt; 는 어디서 생성하고 주입하고 있는 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 파일 내에서 &lt;code&gt;Jackson2ObjectMapperBuilderConfiguration&lt;/code&gt; 객체를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({Jackson2ObjectMapperBuilder.class})
static class JacksonObjectMapperBuilderConfiguration {
    ...
    @Bean
    @Scope(&quot;prototype&quot;)
    @ConditionalOnMissingBean
    Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(
        ApplicationContext applicationContext, 
        List&amp;lt;Jackson2ObjectMapperBuilderCustomizer&amp;gt; customizers
    ) {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.applicationContext(applicationContext);
        this.customize(builder, customizers);
        return builder;
    }
    ...
}&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;Jackson2ObjectMapperBuilder&lt;/code&gt;는 또 &lt;code&gt;Jackson2ObjectMapperBuilderCustomizer&lt;/code&gt; List를 주입받아 &lt;code&gt;customize&lt;/code&gt; 라는 함수를 호출해주고 있습니다. 우리는 Spring을 학습하면서 같은 타입의 Bean이 여러개 주입될 경우 List 자료구조로 주입 된 Bean을 전부 가져올 수 있다는걸 알고 있습니다. 그럼 여기서 주입받은 &lt;code&gt;customizers&lt;/code&gt;는 어디에서 주입되는 것일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 같은 파일에서 &lt;code&gt;Jackson2ObjectMapperBuilderCustomizerConfiguration&lt;/code&gt;을 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({Jackson2ObjectMapperBuilder.class})
@EnableConfigurationProperties({JacksonProperties.class})
static class Jackson2ObjectMapperBuilderCustomizerConfiguration {
        ...
        @Bean
        StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(
            JacksonProperties jacksonProperties, 
            ObjectProvider&amp;lt;Module&amp;gt; modules
        ) {
            return new StandardJackson2ObjectMapperBuilderCustomizer(jacksonProperties, modules.stream().toList());
        }
        ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StandardJackson2ObjectMapperBuilderCustomizer&lt;/code&gt;를 주입하는 것을 볼 수 있는데 이 Customizer는 yml 파일 등에서 주입된 설정정보를 입력받아 &lt;code&gt;JacksonProperties&lt;/code&gt; 라는 객체에 정보를 담아 &lt;code&gt;StandardJackson2ObjectMapperBuilderCustomizer&lt;/code&gt;에게 전달하고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@ConfigurationProperties(
    prefix = &quot;spring.jackson&quot;
)
public class JacksonProperties { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JacksonProperties&lt;/code&gt;는 prefix로 &lt;code&gt;spring.jackson&lt;/code&gt;을 사용하고 dateFormat, timeZone, naming-strategy 등 과 같은 부분을 설정할 수 있습니다. 사용자는 &lt;b&gt;application.yml&lt;/b&gt; 등과 같은 설정 파일에서 Jackson 관련 설정을 추가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더불어 &lt;code&gt;StandardJackson2ObjectMapperBuilderCustomizer&lt;/code&gt;는 Module의 형식으로 의존 주입된 객체들을 찾아 &lt;code&gt;ObjectMapper&lt;/code&gt;를 구성하기 위한 설정 정보로 사용합니다. 그리고 &lt;code&gt;Module&lt;/code&gt; Bean 주입으로 &lt;code&gt;ObjectMapper&lt;/code&gt;를 커스텀하는 방법은 Spring이 권장하는 방법 중 하나입니다. 그렇다면 이 &lt;code&gt;Module&lt;/code&gt;이란 무엇일까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Module을 이용한 커스터마이징 및 확장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson 라이브러리에서 &lt;code&gt;Module&lt;/code&gt;의 역할은 &lt;b&gt;커스터마이징과 기능 확장을 위한 플러그인의 역할&lt;/b&gt;을 합니다. Jackson이 &lt;code&gt;ObjectMapper&lt;/code&gt;를 구성하는 방법에 대해서 이해하기 위해서 설정을 구성하는 주된 요소인 &lt;code&gt;Module&lt;/code&gt;에 대해 이해해야 합니다. 이를 위해 &lt;code&gt;Module&lt;/code&gt;의 구현 코드를 먼저 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public abstract class Module implements Versioned {
    public Module() {
    }

    public abstract String getModuleName();

    public abstract Version version();

    public Object getTypeId() {
        return this.getClass().getName();
    }

    public abstract void setupModule(SetupContext var1);

    public Iterable&amp;lt;? extends Module&amp;gt; getDependencies() {
        return Collections.emptyList();
    }

    public interface SetupContext {
        Version getMapperVersion();
        &amp;lt;C extends ObjectCodec&amp;gt; C getOwner();
        TypeFactory getTypeFactory()

        ...

        boolean isEnabled(MapperFeature var1);
        void addDeserializers(Deserializers var1);
        void addSerializers(Serializers var1);
        void insertAnnotationIntrospector(AnnotationIntrospector var1);
        void appendAnnotationIntrospector(AnnotationIntrospector var1);
        void setMixInAnnotations(Class&amp;lt;?&amp;gt; var1, Class&amp;lt;?&amp;gt; var2);
        ...
    }
}&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;Module.java&lt;/b&gt; 파일에 있는 코드입니다. &lt;code&gt;Module&lt;/code&gt;은 단순히 &lt;code&gt;Version&lt;/code&gt;을 통해 모듈 버저닝 및 의존성을 위한 기능만 제공할 뿐 부가적인 기능들은 &lt;code&gt;SetupContext&lt;/code&gt;를 통해 이뤄진다는 것을 알 수 있습니다. 그럼 &lt;code&gt;SetupContext&lt;/code&gt;에 해당하는 부분은 어디에서 구현이 될까요? 바로 &lt;code&gt;ObjectMapper&lt;/code&gt;에서 수행하게 됩니다. 이를 확인하기 위해 위에서 참고한 &lt;code&gt;StandardJackson2ObjectMapperBuilderCustomizer&lt;/code&gt;를 &lt;code&gt;customize&lt;/code&gt; 하는 코드를 확인해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;static final class StandardJackson2ObjectMapperBuilderCustomizer
                implements Jackson2ObjectMapperBuilderCustomizer, Ordered {

            private final JacksonProperties jacksonProperties;

            private final Collection&amp;lt;Module&amp;gt; modules;

            StandardJackson2ObjectMapperBuilderCustomizer(
                JacksonProperties jacksonProperties,
                Collection&amp;lt;Module&amp;gt; modules
            ) {
                this.jacksonProperties = jacksonProperties;
                this.modules = modules;
            }
            ...
            @Override
            public void customize(Jackson2ObjectMapperBuilder builder) {
                if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
                    builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
                }
                if (this.jacksonProperties.getTimeZone() != null) {
                    builder.timeZone(this.jacksonProperties.getTimeZone());
                }
                configureFeatures(builder, FEATURE_DEFAULTS);
                configureVisibility(builder, this.jacksonProperties.getVisibility());
                configureFeatures(builder, this.jacksonProperties.getDeserialization());
                configureFeatures(builder, this.jacksonProperties.getSerialization());
                configureFeatures(builder, this.jacksonProperties.getMapper());
                configureFeatures(builder, this.jacksonProperties.getParser());
                configureFeatures(builder, this.jacksonProperties.getGenerator());
                configureFeatures(builder, this.jacksonProperties.getDatatype().getEnum());
                configureFeatures(builder, this.jacksonProperties.getDatatype().getJsonNode());
                configureDateFormat(builder);
                configurePropertyNamingStrategy(builder);
                configureModules(builder); // configModules 호출
                configureLocale(builder);
                configureDefaultLeniency(builder);
                configureConstructorDetector(builder);
            }
            ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 &lt;b&gt;JacksonAutoConfigration.java&lt;/b&gt; 파일 내에서 &lt;code&gt;StandardJackson2ObjectMapperBuilderCustomizer&lt;/code&gt;가 &lt;code&gt;customize&lt;/code&gt; 하는 로직을 나타낸 것입니다. &lt;code&gt;customize&lt;/code&gt; 함수 내에서 &lt;code&gt;builder&lt;/code&gt;를 인자로 받아 &lt;b&gt;configureModules를 호출&lt;/b&gt;하고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private void configureModules(Jackson2ObjectMapperBuilder builder) {
        builder.modulesToInstall(this.modules.toArray(new Module[0]));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;configureModules&lt;/code&gt;를 통해 &lt;code&gt;builder&lt;/code&gt;에 &lt;b&gt;Bean을 통해 주입된 모든 &lt;code&gt;Module&lt;/code&gt;을 Install 하는 역할을 수행&lt;/b&gt;합니다. 이렇게 Install 받은 &lt;code&gt;Module&lt;/code&gt;들은 어떻게 &lt;code&gt;ObjectMapper&lt;/code&gt;에 전달이 될까요? 이를 이해하기 위해서는 &lt;code&gt;ObjectMapperBuilder&lt;/code&gt;가 &lt;code&gt;ObjectMapper&lt;/code&gt;를 &lt;code&gt;build&lt;/code&gt;하는 과정을 살펴봐야 합니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;public &amp;lt;T extends ObjectMapper&amp;gt; T build() {
    ObjectMapper mapper;
    if (this.createXmlMapper) {
        mapper = (this.defaultUseWrapper != null ?
                new XmlObjectMapperInitializer().create(this.defaultUseWrapper, this.factory) :
                new XmlObjectMapperInitializer().create(this.factory));
    }
    else {
        mapper = (this.factory != null ? new ObjectMapper(this.factory) : new ObjectMapper());
    }
    configure(mapper);
    return (T) mapper;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 &lt;code&gt;JacksonObjectMapperBuilder&lt;/code&gt;가 &lt;code&gt;build()&lt;/code&gt; 를 통해 &lt;code&gt;ObjectMapper&lt;/code&gt;를 생성하는 코드입니다. &lt;code&gt;ObjectMapper&lt;/code&gt;를 생성한 후 &lt;code&gt;configure()&lt;/code&gt; 를 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public void configure(ObjectMapper objectMapper) {
    ...

    MultiValueMap&amp;lt;Object, Module&amp;gt; modulesToRegister = new LinkedMultiValueMap&amp;lt;&amp;gt;();
    if (this.findModulesViaServiceLoader) {
        ObjectMapper.findModules(this.moduleClassLoader).forEach(module -&amp;gt; registerModule(module, modulesToRegister));
    }
    else if (this.findWellKnownModules) {
        registerWellKnownModulesIfAvailable(modulesToRegister);
    }

    if (this.modules != null) {
        this.modules.forEach(module -&amp;gt; registerModule(module, modulesToRegister));
    }
    if (this.moduleClasses != null) {
        for (Class&amp;lt;? extends Module&amp;gt; moduleClass : this.moduleClasses) {
            registerModule(BeanUtils.instantiateClass(moduleClass), modulesToRegister);
        }
    }
    List&amp;lt;Module&amp;gt; modules = new ArrayList&amp;lt;&amp;gt;();
    for (List&amp;lt;Module&amp;gt; nestedModules : modulesToRegister.values()) {
        modules.addAll(nestedModules);
    }
    objectMapper.registerModules(modules);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;configure()&lt;/code&gt; 함수 내에서 &lt;code&gt;modules&lt;/code&gt;를 탐색 후 &lt;code&gt;objectMapper.registerModules()&lt;/code&gt;를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public ObjectMapper registerModule(Module module)
{
    //     ... (중략) ...
    // And then call registration
    module.setupModule(new Module.SetupContext()
    {
      ...
        @SuppressWarnings(&quot;unchecked&quot;)
        @Override
        public &amp;lt;C extends ObjectCodec&amp;gt; C getOwner() {
            // why do we need the cast here?!?
            return (C) ObjectMapper.this;
        }

        @Override
        public TypeFactory getTypeFactory() {
            return _typeFactory;
        }

        @Override
        public boolean isEnabled(MapperFeature f) {
            return ObjectMapper.this.isEnabled(f);
        }

        // ... (중략) ...

        // // // Methods for registering handlers: deserializers

        @Override
        public void addDeserializers(Deserializers d) {
            DeserializerFactory df = _deserializationContext._factory.withAdditionalDeserializers(d);
            _deserializationContext = _deserializationContext.with(df);
        }

        @Override
        public void addSerializers(Serializers s) {
            _serializerFactory = _serializerFactory.withAdditionalSerializers(s);
        }

        @Override
        public void insertAnnotationIntrospector(AnnotationIntrospector ai) {
            _deserializationConfig = _deserializationConfig.withInsertedAnnotationIntrospector(ai);
            _serializationConfig = _serializationConfig.withInsertedAnnotationIntrospector(ai);
        }

        @Override
        public void appendAnnotationIntrospector(AnnotationIntrospector ai) {
            _deserializationConfig = _deserializationConfig.withAppendedAnnotationIntrospector(ai);
            _serializationConfig = _serializationConfig.withAppendedAnnotationIntrospector(ai);
        }

        @Override
        public void setMixInAnnotations(Class&amp;lt;?&amp;gt; target, Class&amp;lt;?&amp;gt; mixinSource) {
            addMixIn(target, mixinSource);
        }
        ...
    });

    return this;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;objectMapper&lt;/code&gt;는 &lt;code&gt;registerModules()&lt;/code&gt; 함수 내에서 &lt;code&gt;module&lt;/code&gt;를 순차적으로 돌며 &lt;code&gt;module&lt;/code&gt;의 &lt;code&gt;setupModule&lt;/code&gt;를 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;setupModule()&lt;/code&gt; 이라는 메서드는 &lt;code&gt;Module&lt;/code&gt;과 &lt;code&gt;ObjectMapper&lt;/code&gt;를 연결해주는 &lt;b&gt;브릿지 역할을 수행&lt;/b&gt;합니다. 우리는 이 메서드 안에서 우리가 필요한 동작들(Serializer 등록, Deserializer 등록 등)을 수행할 수 있습니다. &lt;code&gt;SetupContext&lt;/code&gt;의 역할을 조금 살펴보자면 다음 4가지 정도의 역할을 꼽아볼 수 있습니다.&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;addSerializers()&lt;/b&gt;: &lt;code&gt;Module&lt;/code&gt;에서 제공하는 Serializer를 추가하는 작업을 수행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;addDeserializers()&lt;/b&gt;: &lt;code&gt;Module&lt;/code&gt;에서 제공하는 Deserializer를 추가하는 작업을 수행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;insertAnnotationIntrospector()&lt;/b&gt;, &lt;b&gt;appendAnnotationIntrospector()&lt;/b&gt;: 어노테이션 기반 커스터마이징 된 Serialization, Deserialization을 수행하는 &lt;code&gt;AnnotationIntrospector&lt;/code&gt;를 추가합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;setMixInAnnotations()&lt;/b&gt;: 원본 클래스를 대체한 믹스인을 만들어 원본 클래스에 적용할 Jackson 어노테이션을 추가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 Spring은 Jackson의 기본 제공 클래스(&lt;code&gt;ObjectMapper&lt;/code&gt;, &lt;code&gt;ObjectMapperBuilder&lt;/code&gt; 등)을 건들지 않고 &lt;code&gt;Module&lt;/code&gt;을 추가하는 방식으로 &lt;b&gt;대부분의 Jackson 설정 정보를 커스터마이징&lt;/b&gt; 할 수 있습니다. 이는 Spring에서 제공하는 기본 동작을 방해하지 않고 독립적인 환경에서 커스터마이징함으로써 &lt;b&gt;캡슐화의 이점을 갖을 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 &lt;code&gt;StandardJackson2ObjectMapperBuilderCustomizer&lt;/code&gt;를 주입받을 때 &lt;code&gt;AutoConfiguration&lt;/code&gt;에 의해 자동 주입된 &lt;code&gt;Module&lt;/code&gt; 들은 어떤 것이 있을까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Boot이 기본으로 제공하는 Jackson Module&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JsonComponentModule&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JsonComponentModule&lt;/code&gt;은 &lt;code&gt;JacksonAutoConfiguration&lt;/code&gt;에서 자동 설정을 통해 주입되고 있으며 &lt;code&gt;@JsonComponent&lt;/code&gt; 어노테이션을 통해 등록된 Bean 객체를 주입받아 &lt;code&gt;JsonComponentModule&lt;/code&gt;의 Serializer 및 Deserializer로 등록합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@AutoConfiguration
@ConditionalOnClass({ObjectMapper.class})
public class JacksonAutoConfiguration {
    ...
    @Bean
    public JsonComponentModule jsonComponentModule() {
        return new JsonComponentModule();
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 &lt;b&gt;JacksonAutoConfiguration.java&lt;/b&gt; 파일 내에서 &lt;code&gt;JsonComponentModule&lt;/code&gt; 객체를 Bean 등록하는 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JsonComponentModule&lt;/code&gt;은 코드에서 살펴보시다시피 &lt;code&gt;JacksonAutoConfiguration&lt;/code&gt;에서 자동 설정을 통해 주입되고 있으며 &lt;code&gt;@JsonComponent&lt;/code&gt; 어노테이션을 통해 등록된 &lt;code&gt;Configuration&lt;/code&gt; 객체를 주입받아 &lt;code&gt;JsonComponentModule&lt;/code&gt;의 Serializer 및 Deserializer를 등록합니다. Bean 객체의 주입 대상은 &lt;code&gt;@JsonComponent 어노테이션&lt;/code&gt;의 대상이 된 객체 뿐 아니라 해당 클래스 내에 존재하는 &lt;code&gt;inner class&lt;/code&gt; 또한 등록 대상이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ParameterNamesModule&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ParameterNamesModule.class)
static class ParameterNamesModuleConfiguration {

        @Bean
        @ConditionalOnMissingBean
        ParameterNamesModule parameterNamesModule() {
            return new ParameterNamesModule(JsonCreator.Mode.DEFAULT);
        }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ParameterNamesModule&lt;/code&gt; 또한 &lt;b&gt;JacksonAutoConfiguration.java&lt;/b&gt;에서 Bean 등록을 수행합니다. &lt;code&gt;ParameterNamesModule&lt;/code&gt;은 기본적인 Serializer, Deserializer를 추가하지 않고 클래스의 메서드 및 생성자의 파라미터 이름을 사용하여 Serialization, Deserialization을 지원합니다. 여기서 사용한 &lt;b&gt;JsonCreator.Mode.DEFAULT&lt;/b&gt;는 기본 모드로, 주 생성자가 아닌 생성자에 대해서도 파라미터 이름을 사용하여 Deserialization을 지원합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JsonMixinModule&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
static class JacksonMixinConfiguration {

        @Bean
        static JsonMixinModuleEntries jsonMixinModuleEntries(ApplicationContext context) {
            List&amp;lt;String&amp;gt; packages = AutoConfigurationPackages.has(context) ? AutoConfigurationPackages.get(context)
                    : Collections.emptyList();
            return JsonMixinModuleEntries.scan(context, packages);
        }

        @Bean
        JsonMixinModule jsonMixinModule(ApplicationContext context, JsonMixinModuleEntries entries) {
            JsonMixinModule jsonMixinModule = new JsonMixinModule();
            jsonMixinModule.registerEntries(entries, context.getClassLoader());
            return jsonMixinModule;
        }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JsonMixinModule&lt;/code&gt; 또한 동일하게 &lt;code&gt;JacksonAutoConfiguration&lt;/code&gt;에 의해 자동 설정 시 Bean 등록을 수행합니다. &lt;code&gt;JsonMixinModule&lt;/code&gt;은 기본적으로 &lt;code&gt;ApplicationContext&lt;/code&gt; 내에서 지정된 패키지를 스캔하여 &lt;code&gt;@JsonMixin&lt;/code&gt; 이라는 어노테이션이 지정된 믹스인 클래스를 찾아 &lt;code&gt;JsonMixinModule&lt;/code&gt;에 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 Spring Boot에서는 properties 파일 및 기본적으로 제공하는 &lt;code&gt;Module&lt;/code&gt; 들에 의해 &lt;code&gt;ObjectMapper&lt;/code&gt;가 생성됩니다. 그래서 &lt;code&gt;ObjectMapper&lt;/code&gt;를 임의로 재정의할 때에는 이러한 기본 동작이 제대로 이뤄지지 않을 수 있음을 알고 Jackson 설정을 커스터마이징 해야 합니다. 그럼 이러한 기본 동작을 방해하지 않는 선에서 &lt;code&gt;JacksonObjectMapperBuilder&lt;/code&gt; 및 Serializer, Deserializer를 커스터마이징 하는 방법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Jackson2ObjectMapperBuilder 직접 커스터마이징&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({Jackson2ObjectMapperBuilder.class})
static class JacksonObjectMapperBuilderConfiguration {
    ...
    @Bean
    @Scope(&quot;prototype&quot;)
    @ConditionalOnMissingBean
    Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(
        ApplicationContext applicationContext, 
        List&amp;lt;Jackson2ObjectMapperBuilderCustomizer&amp;gt; customizers
    ) {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.applicationContext(applicationContext);
        this.customize(builder, customizers);
        return builder;
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt;는 다음과 같이 주입된 &lt;code&gt;customizers&lt;/code&gt;를 &lt;code&gt;customize&lt;/code&gt; 하는 동작을 내포하고 있습니다. &lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt;를 기본적인 동작을 해치지 않고 재정의 하기 위해서는 위의 코드와 동일하게 기본적으로 제공하는 &lt;code&gt;customizers&lt;/code&gt;를 &lt;code&gt;customize()&lt;/code&gt; 하는 작업이 필요합니다. 예시 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;@Configuration
class JacksonConfig {

    @Bean
    fun jackson2ObjectMapperBuilder(customizers: List&amp;lt;Jackson2ObjectMapperBuilderCustomizer&amp;gt;): Jackson2ObjectMapperBuilder {
        val builder = Jackson2ObjectMapperBuilder()
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .serializerByType(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
            .serializerByType(LocalDate::class.java, LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE))
            .serializerByType(LocalTime::class.java, LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME))
        customizers.forEach { customizer -&amp;gt; customizer.customize(builder) }
        return builder
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 &lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt;의 필요 없는 기본 설정을 비활성화 하고 특정 타입에 대한 Serializer를 추가하는 코드입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)&lt;/code&gt; : Jackson은 기본적으로 날짜 정보를 Timestamp로 표시합니다. 이를 비활성화 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)&lt;/code&gt; : JSON에 매핑되지 않은 알 수 없는 속성이 존재해도 오류를 발생시키지 않도록 설정합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;serializerByType()&lt;/code&gt;: 날짜 및 시간 타입의 객체를 &lt;code&gt;com.fasterxml.jackson.datatype.jsr310.ser&lt;/code&gt; 에서 제공하는 Serializer들로 대체합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;customizer.customize(builder)&lt;/code&gt; : &lt;code&gt;Customizer&lt;/code&gt;를 순회하면서 &lt;code&gt;Customizer&lt;/code&gt;의 설정 정보들을 내려받습니다. &lt;code&gt;StandardJackson2ObjectMapperBuilderCustomizer&lt;/code&gt; 가 그 중 하나이며 해당 &lt;code&gt;Customizer&lt;/code&gt;가 포함하는 &lt;code&gt;JacksonProperties&lt;/code&gt; 및 &lt;code&gt;기본 제공 Module&lt;/code&gt;의 설정 정보를 주입합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Jackson2ObjectMapperBuilderCustomizer 주입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 위와 같이 설정하는건 &lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt;에 &lt;code&gt;Customizer&lt;/code&gt;를 어떻게 주입 시켜야 하는지 알고 있어야 채택할 수 있는 방법이죠. 더불어 위의 로직은 &lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt;를 자동으로 주입하는 로직에 포함된 로직이니 &lt;b&gt;중복&lt;/b&gt;이라 할 수 있습니다. Spring Framework는 이를 위해 &lt;code&gt;Jackson2ObjectMapperBuilderCustomizer&lt;/code&gt;를 주입하는 방식으로 &lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt;를 커스터마이징 할 수 있는 방법을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Customizer&lt;/code&gt;들을 &lt;code&gt;customize&lt;/code&gt; 하는 로직을 중복으로 작성하지 않아도 되니 위의 방식보다 더 우아한 방식이라고 생각합니다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;class SimpleObjectMapperBuilderCustomizer : Jackson2ObjectMapperBuilderCustomizer {
    override fun customize(jacksonObjectMapperBuilder: Jackson2ObjectMapperBuilder) {
        jacksonObjectMapperBuilder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .serializerByType(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
            .serializerByType(LocalDate::class.java, LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE))
            .serializerByType(LocalTime::class.java, LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@TestConfiguration
class ObjectMapperBuilderCustomizerConfiguration {

    @Bean
    fun objectMapperBuilderCustomizer(): SimpleObjectMapperBuilderCustomizer {
        return SimpleObjectMapperBuilderCustomizer()
    }
      ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Serializer, Deserializer를 커스터마이징 하는 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본으로 제공하는 Module 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Serializer 및 Deserializer를 추가하는 방법은 매우 다양합니다. 기본 제공 &lt;code&gt;Module&lt;/code&gt;인 &lt;code&gt;JsonComponentModule&lt;/code&gt;, &lt;code&gt;JsonMixinModule&lt;/code&gt;을 사용해서 Json 관련 어노테이션을 사용해서 Serializer 및 Deserializer를 추가할 수 있습니다. 하지만 본문에서는 &lt;code&gt;Module&lt;/code&gt;을 통한 커스터마이징 방법에 대해 기술하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Module 구현 및 빈 주입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법은 &lt;code&gt;SimpleModule&lt;/code&gt;이라고 하는 &lt;b&gt;간편화된 &lt;code&gt;Module&lt;/code&gt; 객체를 상속&lt;/b&gt;하는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 예제는 기본적인 User 데이터 클래스의 Serializer, Deserializer를 추가하는 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class User(
    val id: Int,
    val name: String,
    val password: String
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 User에 대한 Serializer를 구현합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider

class UserSerializer : JsonSerializer&amp;lt;User&amp;gt;() {
    override fun serialize(user: User, gen: JsonGenerator, serializers: SerializerProvider) {
        gen.writeStartObject()
        gen.writeNumberField(&quot;user_id&quot;, user.id)
        gen.writeStringField(&quot;name&quot;, user.name)
        // Password 필드는 직렬화하지 않음
        gen.writeEndObject()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 User에 대한 Deserializer를 구현합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer

class UserDeserializer : JsonDeserializer&amp;lt;User&amp;gt;() {
    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): User {
        val node = p.codec.readTree&amp;lt;com.fasterxml.jackson.databind.node.ObjectNode&amp;gt;(p)
        val id = node.get(&quot;user_id&quot;).asInt()
        val name = node.get(&quot;name&quot;).asText()
        // 역직렬화할 때 패스워드 필드는 기본 값으로 설정
        return User(id, name, &quot;&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 위에서 구현한 &lt;code&gt;UserSerializer&lt;/code&gt;, &lt;code&gt;UserDeserializer&lt;/code&gt;를 포함하는 &lt;code&gt;Module&lt;/code&gt;를 구현합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import com.fasterxml.jackson.databind.module.SimpleModule

class UserModule : SimpleModule() {
    init {
        addSerializer(User::class.java, UserSerializer())
        addDeserializer(User::class.java, UserDeserializer())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SimpleModule&lt;/code&gt;을 사용하면 구태여 &lt;code&gt;setupModule()&lt;/code&gt; 을 오버라이딩 해서 context에 직접 접근해 &lt;code&gt;addSerializer()&lt;/code&gt; 및 &lt;code&gt;addDeserializer()&lt;/code&gt;를 추가할 필요없이 바로 추가가 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어진 &lt;code&gt;UserModule&lt;/code&gt;을 단순히 Bean 으로 주입해주면 알아서 &lt;code&gt;Module&lt;/code&gt;들이 ObjectMapper에 Install 되어 실행될 것입니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class JacksonConfig {

    @Bean
    fun userModule(): UserModule {
            return UserModule()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 설정 방법대로 설정 할 경우 제대로 동작을 하는지 확인 하기 위해 테스트 코드를 작성해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 중점적으로 테스트 해야할 부분은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커스터마이징한 &lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt;의 설정 정보가 제대로 주입되는가?&lt;/li&gt;
&lt;li&gt;yml에 작성한 jackson 설정 정보가 제대로 주입되는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UserModule&lt;/code&gt;이 제대로 주입되는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml이 제대로 설정 정보에 주입되는지 확인하기 위해 yml 설정을 추가하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;spring:
  jackson:
    property-naming-strategy: SNAKE_CASE&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ObjectMapperBuilder를 직접 생성한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;code&gt;ObjectMapperBuilder&lt;/code&gt;를 직접 생성한 경우에 대한 테스트를 진행해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@TestConfiguration
class PlainObjectMapperBuilderConfiguration {

    @Bean
    fun objectMapperBuilder(): Jackson2ObjectMapperBuilder {
        return Jackson2ObjectMapperBuilder()
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .serializerByType(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
            .serializerByType(LocalDate::class.java, LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE))
            .serializerByType(LocalTime::class.java, LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME))
    }

    @Bean
    fun userModule(): UserModule {
        return UserModule()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@SpringBootTest
@Import(PlainObjectMapperBuilderConfiguration::class)
class PlainObjectMapperTest {

    @Autowired
    private lateinit var objectMapper: ObjectMapper

    @DisplayName(&quot;Customized ObjectMapperBuilder 의 주입을 통해 Customized 설정 정보가 제대로 설정된다.&quot;)
    @Test
    fun testObjectMapperBuilderCustomizerSettings() {
        assertThat(objectMapper.serializationConfig.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)).isFalse()
        assertThat(objectMapper.deserializationConfig.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse()

        // Verify LocalDateTime serializer
        val dateTime = LocalDateTime.of(2020, 1, 1, 12, 0)
        val dateTimeJson = objectMapper.writeValueAsString(dateTime)
        assertThat(dateTimeJson).isEqualTo(&quot;\&quot;2020-01-01T12:00:00\&quot;&quot;)

        // Verify LocalDate serializer
        val date = LocalDate.of(2020, 1, 1)
        val dateJson = objectMapper.writeValueAsString(date)
        assertThat(dateJson).isEqualTo(&quot;\&quot;2020-01-01\&quot;&quot;)

        // Verify LocalTime serializer
        val time = LocalTime.of(12, 0)
        val timeJson = objectMapper.writeValueAsString(time)
        assertThat(timeJson).isEqualTo(&quot;\&quot;12:00:00\&quot;&quot;)
    }

    @DisplayName(&quot;ObjectMapperBuilder를 직접 생성하면 yml 세팅을 무시한다.&quot;)
    @Test
    fun testPlainObjectMapperCreationIgnoresYmlSettings() {
        assertThat(objectMapper.propertyNamingStrategy).isNull()
    }

    @DisplayName(&quot;ObjectMapperBuilder를 직접 생성하면 직접 주입한 Module의 Serializer를 Instsall 할 수 없다.&quot;)
    @Test
    fun testPlainObjectMapperBuilderCreationDoesNotInstallInjectedModuleSerializers() {
        val user = User(1, &quot;John Doe&quot;, &quot;secret&quot;)
        val json = objectMapper.writeValueAsString(user)

        val expectedJson = &quot;&quot;&quot;{&quot;user_id&quot;:1,&quot;name&quot;:&quot;John Doe&quot;}&quot;&quot;&quot;
        assertThat(expectedJson).isNotEqualTo(json)
    }

    @DisplayName(&quot;ObjectMapperBuilder를 직접 생성하면 직접 주입한 Module의 Deserializer를 Instsall 할 수 없다.&quot;)
    @Test
    fun testPlainObjectMapperBuilderCreationDoesNotInstallInjectedModuleDeserializers() {
        val json = &quot;&quot;&quot;{&quot;user_id&quot;:1,&quot;name&quot;:&quot;John Doe&quot;,&quot;password&quot;:&quot;&quot;}&quot;&quot;&quot;

        val user: User = objectMapper.readValue(json)

        val expectedUser = User(1, &quot;John Doe&quot;, &quot;&quot;)
        assertThat(expectedUser).isNotEqualTo(user)
        assertThat(expectedUser.id).isNotEqualTo(user.id)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트의 결과는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Customizing 한 Jackson2ObjectMapperBuilder의 설정 정보가 제대로 주입되는가? -&amp;gt; Yes&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml에 작성한 jackson 설정 정보가 제대로 주입되는가? -&amp;gt; No&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User Module이 제대로 주입되는가? -&amp;gt; No&lt;/p&gt;
&lt;p&gt;&lt;del&gt;&lt;/del&gt;&lt;/p&gt;
&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;Jackson2ObjectMapperBuilder&lt;/code&gt;를 직접 생성해 작성한 설정 정보는 제대로 주입되지만 &lt;code&gt;JacksonAutoConfiguration&lt;/code&gt;이 제공해주는 &lt;b&gt;기본 yml 세팅 및 주입된 &lt;code&gt;Module Install&lt;/code&gt; 작업이 생략된 것&lt;/b&gt;을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ObjectMapperBuilder와 주입된 customizers를 직접 세팅한 경우&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@TestConfiguration
class CustomizedObjectMapperBuilderConfiguration {

    @Bean
    fun objectMapperBuilder(customizers: List&amp;lt;Jackson2ObjectMapperBuilderCustomizer&amp;gt;): Jackson2ObjectMapperBuilder {
        val builder = Jackson2ObjectMapperBuilder()
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .serializerByType(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
            .serializerByType(LocalDate::class.java, LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE))
            .serializerByType(LocalTime::class.java, LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME))
        customizers.forEach { customizer -&amp;gt; customizer.customize(builder) }
        return builder
    }

    @Bean
    fun userModule(): UserModule {
        return UserModule()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Import(CustomizedObjectMapperBuilderConfiguration::class)
@SpringBootTest
class CustomizedObjectMapperBuilderTest {

    @Autowired
    private lateinit var objectMapper: ObjectMapper

    @DisplayName(&quot;Customized ObjectMapperBuilder 의 주입을 통해 Customized 설정 정보가 제대로 설정된다.&quot;)
    @Test
    fun testObjectMapperBuilderCustomizerSettings() {
        assertThat(objectMapper.serializationConfig.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)).isFalse()
        assertThat(objectMapper.deserializationConfig.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse()

        // Verify LocalDateTime serializer
        val dateTime = LocalDateTime.of(2020, 1, 1, 12, 0)
        val dateTimeJson = objectMapper.writeValueAsString(dateTime)
        assertThat(dateTimeJson).isEqualTo(&quot;\&quot;2020-01-01T12:00:00\&quot;&quot;)

        // Verify LocalDate serializer
        val date = LocalDate.of(2020, 1, 1)
        val dateJson = objectMapper.writeValueAsString(date)
        assertThat(dateJson).isEqualTo(&quot;\&quot;2020-01-01\&quot;&quot;)

        // Verify LocalTime serializer
        val time = LocalTime.of(12, 0)
        val timeJson = objectMapper.writeValueAsString(time)
        assertThat(timeJson).isEqualTo(&quot;\&quot;12:00:00\&quot;&quot;)
    }

    @DisplayName(&quot;customizers를 customize 하면 yml의 세팅값을 그대로 적용할 수 있다.&quot;)
    @Test
    fun testYmlSettingsAppliedWithCustomizedCustomizers() {
        assertThat(objectMapper.propertyNamingStrategy).isEqualTo(PropertyNamingStrategies.SNAKE_CASE)
    }

    @DisplayName(&quot;customizers를 customize 하면 Bean으로 등록된 Module의 Serializer가 자동으로 Install 된다.&quot;)
    @Test
    fun testBeanRegisteredModuleSerializersAutoInstallWithCustomizedCustomizers() {
        val user = User(1, &quot;John Doe&quot;, &quot;secret&quot;)
        val json = objectMapper.writeValueAsString(user)

        val expectedJson = &quot;&quot;&quot;{&quot;user_id&quot;:1,&quot;name&quot;:&quot;John Doe&quot;}&quot;&quot;&quot;
        assertThat(expectedJson).isEqualTo(json)
    }

    @DisplayName(&quot;customizers를 customize 하면 Bean으로 등록된 Module의 Deserializer가 자동으로 Install 된다.&quot;)
    @Test
    fun testBeanRegisteredModuleDeserializersAutoInstallWithCustomizedCustomizers() {
        val json = &quot;&quot;&quot;{&quot;user_id&quot;:1,&quot;name&quot;:&quot;John Doe&quot;}&quot;&quot;&quot;
        val user: User = objectMapper.readValue(json)

        val expectedUser = User(1, &quot;John Doe&quot;, &quot;&quot;)
        assertThat(expectedUser).isEqualTo(user)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트의 결과는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; Customizing 한 Jackson2ObjectMapperBuilder의 설정 정보가 제대로 주입되는가? -&amp;gt; Yes&lt;br /&gt;&amp;nbsp; yml에 작성한 jackson 설정 정보가 제대로 주입되는가? -&amp;gt; Yes&lt;br /&gt;&amp;nbsp; User Module이 제대로 주입되는가? -&amp;gt; Yes&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 작성한 경우 바라던 대로 제대로 설정이 이루어지는 것을 확인할 수 있습니다. 다만, &lt;code&gt;ObjectMapperBuilder&lt;/code&gt;를 주입하는 코드에서 &lt;code&gt;customizers&lt;/code&gt;를 주입받아 &lt;code&gt;customize()&lt;/code&gt; 과정을 거쳐야 한다는 단점이 존재합니다. &lt;code&gt;Customizers&lt;/code&gt;를 &lt;code&gt;customize()&lt;/code&gt; 하는 로직은 사실 &lt;code&gt;ObjectMapperBuilder&lt;/code&gt;를 커스터마이징 할 때 알기에는 &lt;b&gt;과한 관심사&lt;/b&gt;로 보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ObjectMapperBuilderCustomizer를 주입하는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ObjectMapperBuilder&lt;/code&gt;를 커스터마이징 할 때 가장 깔끔한 방법은 &lt;code&gt;Jackson2ObjectMapperBuilderCustomizer&lt;/code&gt;를 직접 구현해 &lt;code&gt;ObJectMapperBuilder&lt;/code&gt; 커스터마이징만 하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;class SimpleObjectMapperBuilderCustomizer : Jackson2ObjectMapperBuilderCustomizer {
    override fun customize(jacksonObjectMapperBuilder: Jackson2ObjectMapperBuilder) {
        jacksonObjectMapperBuilder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .serializerByType(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
            .serializerByType(LocalDate::class.java, LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE))
            .serializerByType(LocalTime::class.java, LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@TestConfiguration
class ObjectMapperBuilderCustomizerConfiguration {

    @Bean
    fun objectMapperBuilderCustomizer(): SimpleObjectMapperBuilderCustomizer {
        return SimpleObjectMapperBuilderCustomizer()
    }

    @Bean
    fun userModule(): UserModule {
        return UserModule()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Import(ObjectMapperBuilderCustomizerConfiguration::class)
@SpringBootTest
class ObjectMapperBuilderCustomizerTest {
    @Autowired
    private lateinit var objectMapper: ObjectMapper

    @DisplayName(&quot;customized ObjectMapperBuilderCustomizer의 주입을 통해 Customized 설정 정보가 제대로 설정된다.&quot;)
    @Test
    fun testObjectMapperBuilderCustomizerSettings() {
        assertThat(objectMapper.serializationConfig.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)).isFalse()
        assertThat(objectMapper.deserializationConfig.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse()

        // Verify LocalDateTime serializer
        val dateTime = LocalDateTime.of(2020, 1, 1, 12, 0)
        val dateTimeJson = objectMapper.writeValueAsString(dateTime)
        assertThat(dateTimeJson).isEqualTo(&quot;\&quot;2020-01-01T12:00:00\&quot;&quot;)

        // Verify LocalDate serializer
        val date = LocalDate.of(2020, 1, 1)
        val dateJson = objectMapper.writeValueAsString(date)
        assertThat(dateJson).isEqualTo(&quot;\&quot;2020-01-01\&quot;&quot;)

        // Verify LocalTime serializer
        val time = LocalTime.of(12, 0)
        val timeJson = objectMapper.writeValueAsString(time)
        assertThat(timeJson).isEqualTo(&quot;\&quot;12:00:00\&quot;&quot;)
    }

    @DisplayName(&quot;customized ObjectMapperBuilderCustomizer를 주입하면 yml의 세팅값을 그대로 적용할 수 있다.&quot;)
    @Test
    fun testYmlSettingsAppliedWithCustomizedCustomizers() {
        assertThat(objectMapper.propertyNamingStrategy).isEqualTo(PropertyNamingStrategies.SNAKE_CASE)
    }

    @DisplayName(&quot;customized ObjectMapperBuilderCustomizer를 주입하면 Bean으로 등록된 Module의 Serializer가 자동으로 Install 된다.&quot;)
    @Test
    fun testBeanRegisteredModuleSerializersAutoInstallWithCustomizedCustomizers() {
        val user = User(1, &quot;John Doe&quot;, &quot;secret&quot;)
        val json = objectMapper.writeValueAsString(user)

        val expectedJson = &quot;&quot;&quot;{&quot;user_id&quot;:1,&quot;name&quot;:&quot;John Doe&quot;}&quot;&quot;&quot;
        assertThat(expectedJson).isEqualTo(json)
    }

    @DisplayName(&quot;customized ObjectMapperBuilderCustomizer를 주입하면 Bean으로 등록된 Module의 Deserializer가 자동으로 Install 된다.&quot;)
    @Test
    fun testBeanRegisteredModuleDeserializersAutoInstallWithCustomizedCustomizers() {
        val json = &quot;&quot;&quot;{&quot;user_id&quot;:1,&quot;name&quot;:&quot;John Doe&quot;}&quot;&quot;&quot;
        val user: User = objectMapper.readValue(json)

        val expectedUser = User(1, &quot;John Doe&quot;, &quot;&quot;)
        assertThat(expectedUser).isEqualTo(user)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트의 결과는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; Customizing 한 Jackson2ObjectMapperBuilder의 설정 정보가 제대로 주입되는가? -&amp;gt; Yes&lt;br /&gt;&amp;nbsp; yml에 작성한 jackson 설정 정보가 제대로 주입되는가? -&amp;gt; Yes&lt;br /&gt;&amp;nbsp; User Module이 제대로 주입되는가? -&amp;gt; Yes&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방식으로 작성할 때 비로소 Configuration 하는 로직이 매우 간단해졌으며 불필요한 관심에 대해 자유로워졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;Jackson2ObjectMapperBuilder&lt;/code&gt;를 커스터마이징 할 때는 &lt;code&gt;Jackson2ObjectMapperBuilderCustomizer&lt;/code&gt;를 구현한 Bean을 주입하고 새로운 Serializer 및 Deserializer를 추가할 때에는 &lt;code&gt;Module을 구현한 Bean&lt;/code&gt;을 주입하면 &lt;code&gt;JacksonAutoConfiguration&lt;/code&gt; 에서 제공하는 기본 기능은 물론 커스터마이징 또한 자유롭게 할 수 있을것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서 Spring Boot 에서 Jackson이 &lt;code&gt;AutoConfiguration&lt;/code&gt;을 통해 &lt;code&gt;ObjectMapper&lt;/code&gt;를 어떻게 구성하는지에 대해 알아보았습니다. Spring Boot이 기본적으로 제공하는 &lt;code&gt;Module&lt;/code&gt;들의 자세한 구현부는 추후 다른 포스트에서 구술하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot은 이렇게 생성한 &lt;code&gt;ObjectMapper&lt;/code&gt;를 REST API 내에서 &lt;code&gt;ResponseBody&lt;/code&gt;로 들어온 Json 객체를 Deserialization 하거나 응답 값을 Serialization 하는 등의 역할을 수행하게 됩니다. 추후에 Spring 에서 RequestBody를 &lt;code&gt;ObjectMapper&lt;/code&gt;를 통해 Deserialize 하는 방법에 대해 기술하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;</description>
      <category>팀 이야기</category>
      <category>Jackson</category>
      <category>objectmapper</category>
      <category>Spring</category>
      <category>springboot</category>
      <author>MASHUP</author>
      <guid isPermaLink="true">https://mash-up.tistory.com/81</guid>
      <comments>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81-SpringBoot%EC%9D%B4-ObjectMapper%EB%A5%BC-%EA%B5%AC%EC%84%B1%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95#entry81comment</comments>
      <pubDate>Wed, 16 Oct 2024 00:15:56 +0900</pubDate>
    </item>
    <item>
      <title>[스프링팀] gRPC</title>
      <link>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%8C%80-gRPC</link>
      <description>&lt;h1&gt;gRPC란 무엇인가요?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 &lt;b&gt;Google&lt;/b&gt;에서 개발한 &lt;b&gt;원격 프로시저 호출(Remote Procedure Call, RPC)&lt;/b&gt; 시스템입니다. 간단히 말해, gRPC는 &lt;b&gt;서버와 클라이언트&lt;/b&gt; 간의 &lt;b&gt;통신&lt;/b&gt;을 쉽게 할 수 있게 도와주는 &lt;b&gt;프레임워크&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 서버에게 데이터를 요청할 때, 마치 서버 안에 있는 함수나 메서드를 &lt;b&gt;직접 호출하는 것처럼&lt;/b&gt; 요청을 보낼 수 있습니다. 실제로는 네트워크를 통해 요청이 전달되지만, gRPC를 사용하면 &lt;b&gt;서버에 메서드를 호출&lt;/b&gt;하는 것처럼 프로그래밍할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 &lt;b&gt;HTTP/2&lt;/b&gt; 프로토콜을 기반으로 하여 &lt;b&gt;고성능, 저지연 네트워크 통신&lt;/b&gt;을 지원합니다. &lt;b&gt;HTTP/2&lt;/b&gt;는 기존의 HTTP/1.1과 비교하여 &lt;b&gt;멀티플렉싱(Multiplexing)&lt;/b&gt;, &lt;b&gt;헤더 압축(Header Compression)&lt;/b&gt; 등의 최적화 기능을 도입함으로써 &lt;b&gt;대역폭 효율성&lt;/b&gt;을 극대화하고 전송 지연(Latency)을 최소화합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;gRPC에서 지원하는 통신 방식&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Unary RPC&lt;/b&gt;&lt;br /&gt;클라이언트가 &lt;b&gt;하나의 요청을 보내고&lt;/b&gt; 서버는 &lt;b&gt;하나의 응답을 반환&lt;/b&gt;하는 가장 일반적인 형태의 통신 방식입니다.&lt;/li&gt;
&lt;li&gt;Server Streaming RPC&lt;br /&gt;클라이언트가 하나의 요청을 보내면 서버는 여러 개의 응답을 스트리밍으로 보냅니다.&lt;/li&gt;
&lt;li&gt;Client Streaming RPC&lt;br /&gt;클라이언트가 여러 요청을 스트리밍 방식으로 보내고 서버는 하나의 응답을 반환합니다.&lt;/li&gt;
&lt;li&gt;Bidirectional Streaming RPC&lt;br /&gt;클라이언트와 서버가 양방향으로 여러 요청과 응답을 스트리밍 방식으로 주고받을 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Protocol Buffers(Protobuf)란 무엇인가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Protocol Buffers(Protobuf)는 gRPC에서 사용하는 데이터 직렬화 방식으로, 데이터를 작고 빠르게 전송하기 위해 바이너리 형식으로 압축하여 전달합니다. JSON이나 XML처럼 사람이 읽기 쉬운 텍스트 형식과는 달리, Protobuf는 더 작은 파일 크기와 더 빠른 처리 속도를 제공하여 네트워크 효율성을 극대화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Protobuf가 더 빠른 이유는 데이터를 &lt;b&gt;바이너리 형식&lt;/b&gt;으로 직렬화하여 전송량을 줄이고, &lt;b&gt;파싱 오버헤드&lt;/b&gt;를 최소화하며, &lt;b&gt;작고 효율적인 데이터 구조&lt;/b&gt; 덕분에 &lt;b&gt;더 빠른 처리 속도&lt;/b&gt;를 제공하기 때문입니다. Protobuf는 &lt;b&gt;텍스트 기반의 JSON이나 XML과 달리&lt;/b&gt; 태그나 공백 같은 불필요한 데이터를 포함하지 않으며, &lt;b&gt;숫자로 정의된 필드 번호&lt;/b&gt;를 사용해 데이터를 효율적으로 구분합니다. 또한, &lt;b&gt;메타데이터 오버헤드&lt;/b&gt;를 줄여 데이터가 더 컴팩트해지며, 사전 정의된 데이터 구조로 인해 파싱 과정이 단순화되어 &lt;b&gt;빠른 데이터 접근&lt;/b&gt;이 가능합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IDL(Interface Definition Language)이란 무엇인가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IDL(인터페이스 정의 언어)&lt;/b&gt;은 클라이언트와 서버가 어떤 &lt;b&gt;데이터 구조&lt;/b&gt;로 통신할지, 어떤 &lt;b&gt;서비스&lt;/b&gt;를 제공할지 미리 정의해주는 언어입니다. gRPC에서는 &lt;b&gt;Protocol Buffers(proto 파일)&lt;/b&gt;를 IDL로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;proto 파일&lt;/b&gt;에서, 클라이언트와 서버가 통신하는 &lt;b&gt;규칙&lt;/b&gt;을 정의하면, 이 파일을 바탕으로&lt;b&gt; 코드를 생성&lt;/b&gt;할 수 있습니다. 즉, 서버와 클라이언트가 어떤 형식의 데이터를 주고받을지 &lt;b&gt;사전에 약속&lt;/b&gt;을 정해두는 것이라고 이해할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Protocol Buffers (proto) 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Protocol Buffers 파일(.proto 파일)은 gRPC에서 서비스의 인터페이스와 데이터 구조를 정의하는 파일입니다. 예시로, 아래는 간단한 gRPC 서비스와 데이터 구조를 정의한 .proto 파일입니다.&lt;/p&gt;
&lt;pre class=&quot;protobuf&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}

message UserRequest {
  int32 user_id = 1;
}

message UserResponse {
  string name = 1;
  string email = 2;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;service UserService&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UserService라는 이름의 gRPC 서비스를 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;rpc GetUser (UserRequest) returns (UserResponse)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GetUser라는 &lt;b&gt;Unary RPC 메서드&lt;/b&gt;를 정의합니다. 클라이언트가 UserRequest 메시지를 서버에 보내면, 서버는 UserResponse 메시지로 응답을 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;message UserRequest&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 서버로 보낼 요청 데이터 구조입니다. 여기에는 user_id라는 필드가 포함되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;message UserResponse&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 클라이언트에게 보낼 응답 데이터 구조입니다. 여기에는 name과 email이라는 필드가 포함됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;span data-id=&quot;352de5e6-1102-4354-b4b6-a8d595b30845&quot; data-mark-annotation-type=&quot;inlineComment&quot; data-mark-type=&quot;annotation&quot;&gt;JSON과 gRPC 비교 성능&amp;nbsp;테스트&lt;/span&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 테스트&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;nGrinder &lt;/b&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;9961d727-43fd-46f5-b5c1-c1d0cd3b2d79&quot; data-color=&quot;green&quot; data-node-type=&quot;status&quot;&gt;30대&lt;/span&gt;&lt;b&gt; &amp;rarr; java-service(JSON / gRPC) &lt;/b&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;5642dcae-57d0-4f85-8493-03940c6486ee&quot; data-color=&quot;green&quot; data-node-type=&quot;status&quot;&gt;c5.large, 12대&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JSON&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 72px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-number-column=&quot;false&quot; data-layout=&quot;default&quot; data-autosize=&quot;false&quot; data-table-local-id=&quot;7b3d2537-0b63-4a0f-9aac-1d9ca1f1868b&quot; data-table-width=&quot;760&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;4&quot; data-colwidth=&quot;139,200,170,170&quot; data-cell-background=&quot;#f4f5f7&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;JSON&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot; data-colwidth=&quot;139&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;Vusers&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; data-colwidth=&quot;200&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;TPS&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;Time&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;JAVA&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CPU&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;139&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-background-custom-color=&quot;#fdd0ec&quot;&gt;600&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;200&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;29,116.2&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;19.94 ms&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;80% 후반&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;139&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-background-custom-color=&quot;#fdd0ec&quot;&gt;900&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;200&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;30,800.0&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;28.46 ms&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;80% 후반&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 72px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-table-width=&quot;760&quot; data-table-local-id=&quot;7b3d2537-0b63-4a0f-9aac-1d9ca1f1868b&quot; data-autosize=&quot;false&quot; data-layout=&quot;default&quot; data-number-column=&quot;false&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: ;height: 17px;&quot; colspan=&quot;4&quot; data-cell-background=&quot;#f4f5f7&quot; data-colwidth=&quot;139,200,170,170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;b&gt;gRPC&lt;/b&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: ;height: 17px;&quot; data-colwidth=&quot;139&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;Vusers&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: 17px;&quot; data-colwidth=&quot;200&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;TPS&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: 17px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;Time&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: 17px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;JAVA&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CPU&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: ;height: 19px;&quot; data-colwidth=&quot;139&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-background-custom-color=&quot;#fdd0ec&quot;&gt;600&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: 19px;&quot; data-colwidth=&quot;200&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;59,373.2&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: 19px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;10.07 ms&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: 19px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;80% 중반&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: ;height: 19px;&quot; data-colwidth=&quot;139&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-background-custom-color=&quot;#fdd0ec&quot;&gt;900&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: 19px;&quot; data-colwidth=&quot;200&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;62,308.7&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: 19px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;14.40 ms&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: 19px;&quot; data-colwidth=&quot;170&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;80% 후반&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;nGrinder &lt;/b&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;ca6ab603-d3f7-40e4-ac31-f834b977a991&quot; data-color=&quot;green&quot; data-node-type=&quot;status&quot;&gt;30대&lt;/span&gt;&lt;b&gt;&lt;span data-id=&quot;01206242-f99f-4087-86fa-46a1439201bc&quot; data-mark-annotation-type=&quot;inlineComment&quot; data-mark-type=&quot;annotation&quot;&gt; &amp;rarr; &lt;b&gt;php-service(&lt;/b&gt; &lt;/span&gt;&lt;/b&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;88a63c7a-8d73-4a1b-9671-b44a60d6659c&quot; data-color=&quot;blue&quot; data-node-type=&quot;status&quot;&gt;c5.4xlarge, 13대&lt;/span&gt;&lt;b&gt; &amp;rarr; &lt;b&gt;java-service(JSON / gRPC)&lt;/b&gt; &lt;/b&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;fcf68c84-a6fc-46dd-b561-a0c159a30552&quot; data-color=&quot;green&quot; data-node-type=&quot;status&quot;&gt;c5.large, 12대&lt;/span&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 72px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-number-column=&quot;false&quot; data-layout=&quot;default&quot; data-autosize=&quot;false&quot; data-table-local-id=&quot;56fbbebd-5cfd-4796-8cd3-b1a1b8fe4aa6&quot; data-table-width=&quot;760&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;5&quot; data-colwidth=&quot;136,136,136,136,136&quot; data-cell-background=&quot;#f4f5f7&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;JSON&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;Vusers&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;TPS&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;Time&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;PHP CPU&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;JAVA CPU&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-background-custom-color=&quot;#fdd0ec&quot;&gt;600&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;4,761.5&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;117.22 ms&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;30% 초반&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;20% 중반&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-background-custom-color=&quot;#fdd0ec&quot;&gt;900&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;5,928.3&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;137.21 ms&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;30% 초반&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;20% 중반&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-number-column=&quot;false&quot; data-layout=&quot;default&quot; data-autosize=&quot;false&quot; data-table-local-id=&quot;56fbbebd-5cfd-4796-8cd3-b1a1b8fe4aa6&quot; data-table-width=&quot;760&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: ;&quot;&gt;
&lt;td style=&quot;width: ;height: ;&quot; colspan=&quot;5&quot; data-colwidth=&quot;136,136,136,136,136&quot; data-cell-background=&quot;#f4f5f7&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;gRPC&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: ;&quot;&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;Vusers&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;TPS&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;Time&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;PHP CPU&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;JAVA CPU&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: ;&quot;&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-background-custom-color=&quot;#fdd0ec&quot;&gt;600&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;14,588.4&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;40.27 ms&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;70% 초반&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;30% 중반&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: ;&quot;&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-background-custom-color=&quot;#fdd0ec&quot;&gt;900&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;15,497.2&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;56.9 ms&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;80% 초반&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot; data-colwidth=&quot;136&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;30% 중반&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커넥션 재사용 테스트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;nGrinder &amp;rarr; java-service(gRPC)&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 48px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-number-column=&quot;false&quot; data-layout=&quot;default&quot; data-autosize=&quot;false&quot; data-table-local-id=&quot;a3f7c927-feaf-466a-a400-ae9d8e0bfa67&quot; data-table-width=&quot;760&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 99.8837%; height: 10px;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;gRPC&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 16.7442%; height: 38px;&quot; rowspan=&quot;2&quot; data-colwidth=&quot;88&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;비율&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 83.1395%; height: 19px;&quot; data-colwidth=&quot;672&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;ActiveConnection&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;82.94%  &lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 83.1395%; height: 19px;&quot; data-colwidth=&quot;672&quot;&gt;NewConnection&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;17.06%  &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;nGrinder &amp;rarr; php-service &amp;rarr; java-service(gRPC)&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 199px;&quot; border=&quot;1&quot; data-table-width=&quot;760&quot; data-table-local-id=&quot;a3f7c927-feaf-466a-a400-ae9d8e0bfa67&quot; data-autosize=&quot;false&quot; data-layout=&quot;default&quot; data-number-column=&quot;false&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 99.8837%;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;gRPC&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 38px; width: 16.7442%;&quot; rowspan=&quot;2&quot; data-colwidth=&quot;88&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;비율&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 83.1395%;&quot; data-colwidth=&quot;672&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;77cab707-c618-41e6-b9c1-668fbf2dbcb3&quot; data-color=&quot;green&quot; data-node-type=&quot;status&quot;&gt;ActiveConnection&lt;/span&gt; 78.82%  &lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 83.1395%;&quot; data-colwidth=&quot;672&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;87cd6eab-892e-477d-b2e3-cf81974207af&quot; data-color=&quot;red&quot; data-node-type=&quot;status&quot;&gt;NewConnection&lt;/span&gt; 21.98%  &lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;background-color: #f4f5f7; height: 17px; width: 99.8837%;&quot; colspan=&quot;2&quot; data-cell-background=&quot;#f4f5f7&quot; data-colwidth=&quot;88,672&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;gRPC(force_new)&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 55px; width: 16.7442%;&quot; rowspan=&quot;2&quot; data-colwidth=&quot;88&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;비율&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 83.1395%;&quot; data-colwidth=&quot;672&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;31cc9da1-b38c-470f-8736-fceee0a0d529&quot; data-color=&quot;green&quot; data-node-type=&quot;status&quot;&gt;ActiveConnection&lt;/span&gt; 0.74%  &lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 83.1395%;&quot; data-colwidth=&quot;672&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;45939ec4-de74-4850-9a5e-e81fefa8ef09&quot; data-color=&quot;red&quot; data-node-type=&quot;status&quot;&gt;NewConnection&lt;/span&gt; 99.26%  &lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 99.8837%;&quot; colspan=&quot;2&quot; data-colwidth=&quot;88,672&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;b&gt;JSON&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 55px; width: 16.7442%;&quot; rowspan=&quot;2&quot; data-colwidth=&quot;88&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;비율&lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 83.1395%;&quot; data-colwidth=&quot;672&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;04ce63bc-1799-4a0d-b4d8-de3d317123c8&quot; data-color=&quot;green&quot; data-node-type=&quot;status&quot;&gt;ActiveConnection&lt;/span&gt; 0.31%  &lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 83.1395%;&quot; data-colwidth=&quot;672&quot;&gt;
&lt;div data-align=&quot;center&quot;&gt;&lt;span&gt;&lt;span data-style=&quot;bold&quot; data-local-id=&quot;4148bc58-06c0-4598-abf9-0b3589d6143c&quot; data-color=&quot;red&quot; data-node-type=&quot;status&quot;&gt;NewConnection&lt;/span&gt; 99.69%  &lt;/span&gt;&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 &lt;span data-id=&quot;67f0a128-deb9-475e-b5d9-bf50f3a5f5b1&quot; data-mark-annotation-type=&quot;inlineComment&quot; data-mark-type=&quot;annotation&quot;&gt;결과&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS와 CPU 사용률&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;gRPC&lt;/b&gt;는 동일한 CPU 사용률(80%대)에서 &lt;b&gt;JSON 대비 약 2배 이상의 TPS&lt;/b&gt;를 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;예: 600 Vusers 기준으로 gRPC는 59,373 TPS, JSON은 29,116 TPS를 기록했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;응답 시간&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;gRPC&lt;/b&gt;의 응답 시간은 &lt;b&gt;JSON보다 절반 이하&lt;/b&gt;로 짧습니다.&lt;/li&gt;
&lt;li&gt;예: 600 Vusers 기준으로 gRPC는 10.07ms, JSON은 19.94ms입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;커넥션 재사용&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;gRPC&lt;/b&gt;는 활성 커넥션의 82.94%를 재사용하며, &lt;b&gt;새 연결 생성이 매우 적습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JSON&lt;/b&gt;은 커넥션 재사용률이 거의 없고, 요청마다 &lt;b&gt;새 연결을 99.69%&lt;/b&gt; 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론&lt;/b&gt;: gRPC는 &lt;b&gt;TPS 처리량과 응답 시간에서&lt;/b&gt; JSON보다 훨씬 우수하며, &lt;b&gt;커넥션 재사용&lt;/b&gt; 측면에서도 더 효율적입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 결과 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PHP + JSON 병목 지점&lt;/b&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;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON은 텍스트 기반으로, 직렬화 및 역직렬화에 상대적으로 많은 시간이 소요됩니다. 이는 JSON을 처리할 때, 네트워크 오버헤드나 &lt;b&gt;데이터 전송 지연&lt;/b&gt;을 발생시키며, TPS를 저하시킬 수 있습니다.&lt;/li&gt;
&lt;/ul&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;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON은 데이터가 커지고, 이를 전송하는 데 더 많은 대역폭이 필요합니다. 특히 트래픽이 증가하면 네트워크 전송 속도가 저하되고 병목 현상이 발생할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;gRPC&lt;/b&gt;와 &lt;b&gt;JSON&lt;/b&gt;의 요청당 크기를 비교했을 때, JSON 요청이 약 &lt;b&gt;1278 bytes&lt;/b&gt;이고, gRPC 요청이 약 &lt;b&gt;866 bytes&lt;/b&gt;로, &lt;b&gt;약 47%&lt;/b&gt; 차이가 있습니다.&lt;/li&gt;
&lt;/ul&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;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HTTP/1.1 Keep-Alive&lt;/b&gt;가 활성화되어 있어도, 요청이 순차적으로 처리되기 때문에 &lt;b&gt;동시 요청을 처리하지 못합니다&lt;/b&gt;. 이로인해 &lt;span data-id=&quot;71cafc0f-2529-48c7-9b44-da865ac0badd&quot; data-mark-annotation-type=&quot;inlineComment&quot; data-mark-type=&quot;annotation&quot;&gt;동시 요청이 많은 경우, 대기 중인 요청을 처리하기 위해 커넥션이 자주 생성되며, &lt;/span&gt;&lt;b&gt;&lt;span data-id=&quot;71cafc0f-2529-48c7-9b44-da865ac0badd&quot; data-mark-annotation-type=&quot;inlineComment&quot; data-mark-type=&quot;annotation&quot;&gt;커넥션 재사용율이 매우 낮습니다&lt;/span&gt;&lt;/b&gt;. 실제로 JSON 서버와 통신할 때 &lt;b&gt;커넥션 재사용율이 1% 이하&lt;/b&gt;로 나타났습니다.&lt;/li&gt;
&lt;li&gt;커넥션을 매번 새로 생성하거나 직렬적으로 처리함에 따라 &lt;b&gt;네트워크 레이턴시&lt;/b&gt;가 증가하고, 네트워크 연결 설정과 해제의 반복이 성능 병목을 일으킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;PHP 는 프로세스 기반인데, 커넥션 재사용이 가능할까요?&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PHP-FPM과 프로세스 재사용&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;PHP-FPM&lt;/b&gt;은 PHP의 요청 처리 방식을 최적화하기 위해 FastCGI 프로토콜을 사용합니다.&lt;/li&gt;
&lt;li&gt;중요한 점은 &lt;b&gt;새로운 요청마다 프로세스를 생성하는 것이 아니라, 기존 프로세스를 재사용&lt;/b&gt;한다는 것입니다.&lt;/li&gt;
&lt;li&gt;따라서, 동일한 프로세스가 여러 번의 요청을 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHP-FPM can reuse worker processes repeatedly instead of having to create and terminate them for every single PHP request. Although the cost of starting and terminating a new web server process for each request is relatively small, the overall expense quickly increases as the web server begins to handle increasing amounts of traffic. PHP-FPM can serve more traffic than traditional PHP handlers while creating greater resource efficiency.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PHP에서 gRPC 커넥션 재사용&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;gRPC 채널&lt;/b&gt;은 서버와 클라이언트가 연결되는 &lt;b&gt;통신 통로&lt;/b&gt;입니다. gRPC는 이러한 채널을 통해 클라이언트가 서버에 요청을 보냅니다.&lt;/li&gt;
&lt;li&gt;gRPC는 효율성을 위해 &lt;b&gt;채널을 재사용&lt;/b&gt;할 수 있는 메커니즘을 갖추고 있습니다. 새로 생성하는 대신, 기존에 생성된 채널을 &lt;b&gt;재사용&lt;/b&gt;하는 방식입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;gRPC 커넥션 재사용 방법&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Persistent List에 저장된 채널 재사용 (Persistent Channel)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PHP는 gRPC 채널을 처음 생성할 때, 이 채널을 &lt;b&gt;Persistent List&lt;/b&gt;에 저장합니다.&lt;/li&gt;
&lt;li&gt;다음 요청에서 &lt;b&gt;동일한 채널&lt;/b&gt;이 필요하면, &lt;b&gt;새로 생성하는 대신 Persistent List에 저장된 채널을 재사용&lt;/b&gt;합니다. 이렇게 하면 불필요한 자원 낭비를 줄일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Channel 객체 생성 및 Persistent List 재사용 로직&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHP에서 gRPC 채널을 사용할 때, 다음 절차에 따라 채널을 재사용할 수 있습니다:&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;채널이 이미 존재하는지 확인&lt;/b&gt;: 요청에서 사용할 채널이 &lt;b&gt;Persistent List&lt;/b&gt;에 이미 저장되어 있는지 확인합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;채널이 없으면 새로 생성&lt;/b&gt;: 채널이 없으면, &lt;b&gt;새로운 gRPC 채널을 생성&lt;/b&gt;하여 Persistent List에 저장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;채널이 있으면 재사용&lt;/b&gt;: 이미 같은 조건으로 만들어진 채널이 있으면, &lt;b&gt;새로운 채널을 만들지 않고 기존 채널을 재사용&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;extern HashTable grpc_persistent_list;

if (!(PHP_GRPC_PERSISTENT_LIST_FIND(&amp;amp;grpc_persistent_list, key, key_len, rsrc))) {
    // 채널이 없으면 새로 생성해서 Persistent List에 저장
    create_and_add_channel_to_persistent_list(
        channel, target, args, creds, key, key_len, target_upper_bound TSRMLS_CC);
} else {
    // 채널이 존재하면 이를 재사용
    channel_persistent_le_t *le = (channel_persistent_le_t *)rsrc-&amp;gt;ptr;
    if (strcmp(target, le-&amp;gt;channel-&amp;gt;target) != 0 ||
        strcmp(sha1str, le-&amp;gt;channel-&amp;gt;args_hashstr) != 0 ||
        (creds != NULL &amp;amp;&amp;amp; creds-&amp;gt;hashstr != NULL &amp;amp;&amp;amp;
         strcmp(creds-&amp;gt;hashstr, le-&amp;gt;channel-&amp;gt;creds_hashstr) != 0)) {
      // 해시 충돌이 발생하거나 조건이 맞지 않으면 새로 생성
      create_and_add_channel_to_persistent_list(
          channel, target, args, creds, key, key_len, target_upper_bound TSRMLS_CC);
    } else {
      // 기존 채널 재사용
      efree(args.args);
      free_grpc_channel_wrapper(channel-&amp;gt;wrapper, false);
      gpr_mu_destroy(&amp;amp;channel-&amp;gt;wrapper-&amp;gt;mu);
      free(channel-&amp;gt;wrapper);
      channel-&amp;gt;wrapper = NULL;
      channel-&amp;gt;wrapper = le-&amp;gt;channel;
      php_grpc_channel_ref(channel-&amp;gt;wrapper);
      update_and_get_target_upper_bound(target, target_upper_bound);
    }
}&lt;/code&gt;&lt;/pre&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널을 무조건 재사용하지는 않으며, 특정 상황에서는 &lt;b&gt;새로운 채널을 생성&lt;/b&gt;해야 합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;강제 새로운 채널 생성&lt;/b&gt;: force_new 옵션을 사용하면 기존의 채널을 재사용하지 않고 &lt;b&gt;항상 새로운 채널을 생성&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버 주소나 인증 정보가 변경되는 경우&lt;/b&gt;: 요청에서 사용된 서버나 인증 정보가 변경되면, &lt;b&gt;기존 채널을 사용할 수 없으므로 새로운 채널을 생성&lt;/b&gt;해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (force_new || (creds != NULL &amp;amp;&amp;amp; creds-&amp;gt;has_call_creds)) {
    // force_new 옵션이 있거나 채널 크리덴셜에 call 크리덴셜이 있는 경우 재사용하지 않고 새로운 채널을 생성
    create_channel(channel, target, args, creds);
} else if (!(PHP_GRPC_PERSISTENT_LIST_FIND(&amp;amp;grpc_persistent_list, key, key_len, rsrc))) {
    // force_new가 없으면 재사용 가능한 채널을 찾고, 없으면 새로 생성
    create_and_add_channel_to_persistent_list(
        channel, target, args, creds, key, key_len, target_upper_bound TSRMLS_CC);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FYI; &lt;a href=&quot;https://github.com/grpc/grpc/blob/df0b1dfed8f3b61ca625b2e4a43b29425ed2236b/src/php/ext/grpc/channel.c#L49&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/grpc/grpc/blob/df0b1dfed8f3b61ca625b2e4a43b29425ed2236b/src/php/ext/grpc/channel.c#L49&lt;/a&gt;&lt;/p&gt;</description>
      <category>팀 이야기</category>
      <author>MASHUP</author>
      <guid isPermaLink="true">https://mash-up.tistory.com/80</guid>
      <comments>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%8C%80-gRPC#entry80comment</comments>
      <pubDate>Tue, 15 Oct 2024 21:42:56 +0900</pubDate>
    </item>
    <item>
      <title>[스프링팀] Druid</title>
      <link>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%8C%80-Druid</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Druid 란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Druid 공식 문서: &lt;a href=&quot;https://druid.apache.org/docs/latest/design/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://druid.apache.org/docs/latest/design/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs1hrn/btssHfCpMGZ/PrrKoe8buNA1hNlQ9co2O1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs1hrn/btssHfCpMGZ/PrrKoe8buNA1hNlQ9co2O1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs1hrn/btssHfCpMGZ/PrrKoe8buNA1hNlQ9co2O1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs1hrn%2FbtssHfCpMGZ%2FPrrKoe8buNA1hNlQ9co2O1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;350&quot; height=&quot;144&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 데이터에 대한 빠른 OLAP 쿼리를 위해 설계된 실시간 분석 데이터베이스, interactive한 분석을 가능하게 함&lt;/li&gt;
&lt;li&gt;짧은 시간(3초 이내)에 대용량 데이터에 대해 interactive한 분석을 가능하게 함&lt;/li&gt;
&lt;li&gt;OLAP란
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대용량 업무 데이터베이스를 구성하고 BI(Business Intelligence)를 지원하기 위해 사용되는 기술&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 웨어하우스&lt;/b&gt;나&amp;nbsp;&lt;b&gt;데이터 마트&lt;/b&gt;와 같은 대규모 데이터에 대해 최종 사용자가 정보에&amp;nbsp;&lt;b&gt;직접 접근&lt;/b&gt;하여&amp;nbsp;&lt;b&gt;대화식&lt;/b&gt;으로 정보를 분석하고 의사결정에 활용할 수 있는 실시간 분석처리&lt;/li&gt;
&lt;/ul&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;Druid 사용례&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&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;/li&gt;
&lt;li&gt;비즈니스 인텔리전스/OLAP&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Columar 로 데이터를 저장(mysql 등 일반 저장소는 row기반)&lt;/li&gt;
&lt;li&gt;확장 가능한 분산 시스템. 일반적인 Druid 배포는 수십에서 수백 개의 서버에 클러스터링 하여 배포.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초당 수백만개의 레코드 속도로 데이터를 수집하는 동시에 수조 개의 레코드를 유지하고 쿼리 대기 시간을 1초 미만에서 몇 초까지 유지&lt;/li&gt;
&lt;/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;Druid는 전체 클러스터에서 각 쿼리를 병렬로 처리할 수 있음&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;Druid는 데이터를 실시간 또는 일괄 처리할 수 있고, 수집된 데이터는 쿼리에 즉시 사용할 수 있음&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;Druid는 먼저 데이터를 시간별로 분할합니다. 선택적으로 다른 필드를 기반으로 추가 파티셔닝을 구현할 수 있습니다. 시간 기반 쿼리는 쿼리의 시간 범위와 일치하는 파티션에만 액세스하므로 성능이 크게 향상됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;approximate 알고리즘
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Druid에는 approximate dinstinct count, approximate rank등의 계산을 위한 알고리즘이 포함되어 있습니다. 이러한 알고리즘은 적은 메모리 사용을 제공하며 정확한 계산보다 훨씬 빠름&lt;/li&gt;
&lt;li&gt;hyperloglog 알고리즘 사용&lt;/li&gt;
&lt;/ul&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;Druid 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;725&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MIXyN/btssGuT82rT/uG29Hq3uszHaip3yWgFvUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MIXyN/btssGuT82rT/uG29Hq3uszHaip3yWgFvUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MIXyN/btssGuT82rT/uG29Hq3uszHaip3yWgFvUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMIXyN%2FbtssGuT82rT%2FuG29Hq3uszHaip3yWgFvUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1265&quot; height=&quot;725&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;725&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;297&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lgEPj/btssMDiiomN/jkOGXcKvLx5YffwwZk7ytk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lgEPj/btssMDiiomN/jkOGXcKvLx5YffwwZk7ytk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lgEPj/btssMDiiomN/jkOGXcKvLx5YffwwZk7ytk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlgEPj%2FbtssMDiiomN%2FjkOGXcKvLx5YffwwZk7ytk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;297&quot; height=&quot;170&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Coordinatiors: 클러스터의 데이터 가용성을 관리 (세그먼트 로드, 세그먼트 삭제, 세그먼트 레플리케이션 &amp;hellip;)&lt;/li&gt;
&lt;li&gt;Overlord: 데이터 수집 워크로드 할당을 제어&lt;/li&gt;
&lt;li&gt;Brokers: 외부 클라이언트의 쿼리를 처리&lt;/li&gt;
&lt;li&gt;Routers: 선택 사항입니다. broker, coordinator, overload 에게 요청을 라우팅&lt;/li&gt;
&lt;li&gt;Historical: 쿼리 가능한 데이터를 저장&lt;/li&gt;
&lt;li&gt;MiddleManager: 데이터를 수집&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;Druid 실습 w.Turnilo&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 실습은 Mac OS 환경에서 수행되었으며, Druid의 Cluster 구조가 아닌 Standalone 모드로 수행되었습니다. 실제 서비스 환경에서는 Cluster 환경으로 구축하여 사용하시는 것을 추천드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Druid 다운로드 및 실행&lt;/h4&gt;
&lt;pre id=&quot;code_1693470663437&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;wget https://dlcdn.apache.org/druid/25.0.0/apache-druid-25.0.0-bin.tar.gz&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1693470678888&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tar -xzf apache-druid-25.0.0-bin.tar.gz
cd apache-druid-25.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1693470689670&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./bin/start-druid&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Druid UI 접속&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #000000; text-align: start;&quot; href=&quot;http://localhost:8888/&quot; data-token-index=&quot;0&quot;&gt;&lt;span&gt;http://localhost:8888&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRFfjV/btssDKQwWHN/9397rHnJcLBrNzIsneKEs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRFfjV/btssDKQwWHN/9397rHnJcLBrNzIsneKEs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRFfjV/btssDKQwWHN/9397rHnJcLBrNzIsneKEs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRFfjV%2FbtssDKQwWHN%2F9397rHnJcLBrNzIsneKEs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1410&quot; height=&quot;741&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Load Data에서 원하는 저장소 선택&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 실습에서는 batch classic -&amp;gt; example data 선택 (위키피디아 사전 데이터)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C0YQx/btssLDv0WFH/nd9b7DjMsVy3yx0XOPW4hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C0YQx/btssLDv0WFH/nd9b7DjMsVy3yx0XOPW4hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C0YQx/btssLDv0WFH/nd9b7DjMsVy3yx0XOPW4hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC0YQx%2FbtssLDv0WFH%2Fnd9b7DjMsVy3yx0XOPW4hk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1405&quot; height=&quot;764&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;4. Parse Data, Parse Time, Submit&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 필드들을 정의, 시간에 대한 필드 정의 후 Submit 하여 ingestion을 수행함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 ingestion spec은 json 형식으로 제출되어 ingestion이 수행됨.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1403&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c59fKm/btssBrqksIv/vBPRoXFnndeftoLXvum79k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c59fKm/btssBrqksIv/vBPRoXFnndeftoLXvum79k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c59fKm/btssBrqksIv/vBPRoXFnndeftoLXvum79k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc59fKm%2FbtssBrqksIv%2FvBPRoXFnndeftoLXvum79k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1403&quot; height=&quot;750&quot; data-origin-width=&quot;1403&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Query&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 쿼리를 통해 ingestion 된 데이터를 질의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Druid는 Columar 형식으로 저장하므로, dimension(필드) 기준으로 group by 연산하는 것에 빠른 속도를 보여줌&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lldv0/btssGxcjho6/0JYZi1G74brXWiZG24S3IK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lldv0/btssGxcjho6/0JYZi1G74brXWiZG24S3IK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lldv0/btssGxcjho6/0JYZi1G74brXWiZG24S3IK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flldv0%2FbtssGxcjho6%2F0JYZi1G74brXWiZG24S3IK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;892&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;892&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;776&quot; data-origin-height=&quot;976&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clfqif/btssGvFxIwH/xk6kkMhUkvHGfm0kT2FVJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clfqif/btssGvFxIwH/xk6kkMhUkvHGfm0kT2FVJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clfqif/btssGvFxIwH/xk6kkMhUkvHGfm0kT2FVJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fclfqif%2FbtssGvFxIwH%2Fxk6kkMhUkvHGfm0kT2FVJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;776&quot; height=&quot;976&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;976&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;817&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EGIcV/btssMCjn6S7/jveAkGlM54HxZFGTni9FxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EGIcV/btssMCjn6S7/jveAkGlM54HxZFGTni9FxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EGIcV/btssMCjn6S7/jveAkGlM54HxZFGTni9FxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEGIcV%2FbtssMCjn6S7%2FjveAkGlM54HxZFGTni9FxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;817&quot; height=&quot;1134&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6.&amp;nbsp; 추가적으로 실제 데이터, Turnilo 활용한 실습&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Kaggle에서 적합한 데이터를 찾아 다운&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 실습에서는 ecommerce 데이터를 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://www.kaggle.com/datasets/mkechinov/ecommerce-events-history-in-cosmetics-shop&quot;&gt;https://www.kaggle.com/datasets/mkechinov/ecommerce-events-history-in-cosmetics-shop&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1693471337865&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;eCommerce Events History in Cosmetics Shop&quot; data-og-description=&quot;This dataset contains 20M users' events from eCommerce website&quot; data-og-host=&quot;www.kaggle.com&quot; data-og-source-url=&quot;https://www.kaggle.com/datasets/mkechinov/ecommerce-events-history-in-cosmetics-shop&quot; data-og-url=&quot;https://www.kaggle.com/datasets/mkechinov/ecommerce-events-history-in-cosmetics-shop&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zpK1s/hyTMfbwFsr/mt2W6useYfz4lQXcuOwym1/img.jpg?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://www.kaggle.com/datasets/mkechinov/ecommerce-events-history-in-cosmetics-shop&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.kaggle.com/datasets/mkechinov/ecommerce-events-history-in-cosmetics-shop&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zpK1s/hyTMfbwFsr/mt2W6useYfz4lQXcuOwym1/img.jpg?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;eCommerce Events History in Cosmetics Shop&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This dataset contains 20M users' events from eCommerce website&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.kaggle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) ingestion_spec 정의 후 submit&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 '파일의 위치', 'dimension(필드들)', metrics, granularity(묶는 시간 단위)를 정의&lt;/p&gt;
&lt;pre id=&quot;code_1693471375495&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;index_parallel&quot;,
  &quot;spec&quot;: {
    &quot;ioConfig&quot;: {
      &quot;type&quot;: &quot;index_parallel&quot;,
      &quot;inputSource&quot;: {
        &quot;type&quot;: &quot;local&quot;,
        &quot;baseDir&quot;: &quot;/Users/user/Desktop/data&quot;,
        &quot;filter&quot;: &quot;*.csv&quot;
      },
      &quot;inputFormat&quot;: {
        &quot;type&quot;: &quot;csv&quot;,
        &quot;findColumnsFromHeader&quot;: true
      }
    },
    &quot;tuningConfig&quot;: {
      &quot;type&quot;: &quot;index_parallel&quot;,
      &quot;partitionsSpec&quot;: {
        &quot;type&quot;: &quot;dynamic&quot;
      }
    },
    &quot;dataSchema&quot;: {
      &quot;dataSource&quot;: &quot;shopping&quot;,
      &quot;timestampSpec&quot;: {
        &quot;column&quot;: &quot;event_time&quot;,
        &quot;format&quot;: &quot;auto&quot;
      },
      &quot;dimensionsSpec&quot;: {
        &quot;dimensions&quot;: [
          &quot;event_type&quot;,
          &quot;product_id&quot;,
          &quot;category_id&quot;,
          &quot;category_code&quot;,
          &quot;brand&quot;,
          {
            &quot;type&quot;: &quot;double&quot;,
            &quot;name&quot;: &quot;price&quot;
          },
          &quot;user_id&quot;,
          &quot;user_session&quot;
        ]
      },
      &quot;metricsSpec&quot;: [
        {
          &quot;type&quot;: &quot;hyperUnique&quot;,
          &quot;name&quot;: &quot;user_id_uv&quot;,
          &quot;fieldName&quot;: &quot;user_id&quot;,
          &quot;isInputHyperUnique&quot;: false,
          &quot;round&quot;: false
        },
        {
          &quot;type&quot;: &quot;hyperUnique&quot;,
          &quot;name&quot;: &quot;user_session_uv&quot;,
          &quot;fieldName&quot;: &quot;user_session&quot;,
          &quot;isInputHyperUnique&quot;: false,
          &quot;round&quot;: false
        }
      ],
      &quot;granularitySpec&quot;: {
        &quot;queryGranularity&quot;: &quot;HOUR&quot;,
        &quot;rollup&quot;: true
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) turnilo 설치&lt;/p&gt;
&lt;pre id=&quot;code_1693471568004&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew install node

npm install -g turnilo
turnilo connect-druid http://localhost:8888&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;http://localhost:9090 접속후, UI 로 데이터 분석&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- UI로 pivot table처럼 분석이 가능하여 쿼리를 모르는 기획자, 마케터들도 손쉽게 데이터를 분석하고 이를 레포트에 활용 가능&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;889&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8vsnU/btssN2IFDDL/4HyPIe8hMz8ksjHD45Veck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8vsnU/btssN2IFDDL/4HyPIe8hMz8ksjHD45Veck/img.png&quot; data-alt=&quot;전날 발생한 브랜드별 매출&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8vsnU/btssN2IFDDL/4HyPIe8hMz8ksjHD45Veck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8vsnU%2FbtssN2IFDDL%2F4HyPIe8hMz8ksjHD45Veck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1402&quot; height=&quot;889&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;889&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전날 발생한 브랜드별 매출&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;767&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clq6P1/btssGzOy65S/SaGxCjXpkBCsd2hIOdqkAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clq6P1/btssGzOy65S/SaGxCjXpkBCsd2hIOdqkAk/img.png&quot; data-alt=&quot;시간대별 브랜드별 매출 분석&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clq6P1/btssGzOy65S/SaGxCjXpkBCsd2hIOdqkAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fclq6P1%2FbtssGzOy65S%2FSaGxCjXpkBCsd2hIOdqkAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1406&quot; height=&quot;767&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;767&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시간대별 브랜드별 매출 분석&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rW1Z7/btssN1iHbEH/hQWncH8l4QIbphtZer1ycK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rW1Z7/btssN1iHbEH/hQWncH8l4QIbphtZer1ycK/img.png&quot; data-alt=&quot;시간대별로 발생한 쇼핑 이벤트 추이 분석&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rW1Z7/btssN1iHbEH/hQWncH8l4QIbphtZer1ycK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrW1Z7%2FbtssN1iHbEH%2FhQWncH8l4QIbphtZer1ycK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1160&quot; height=&quot;500&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시간대별로 발생한 쇼핑 이벤트 추이 분석&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>팀 이야기</category>
      <author>MASHUP</author>
      <guid isPermaLink="true">https://mash-up.tistory.com/79</guid>
      <comments>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%8C%80-Druid#entry79comment</comments>
      <pubDate>Thu, 31 Aug 2023 17:50:08 +0900</pubDate>
    </item>
    <item>
      <title>Mash-Up 13기 최종발표</title>
      <link>https://mash-up.tistory.com/entry/Mash-Up-13%EA%B8%B0-%EC%B5%9C%EC%A2%85%EB%B0%9C%ED%91%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;안녕하세요 Mash-Up입니다   8월 12일엔 약 5개월간 각 프로젝트 팀들이 함께 달려온 프로젝트를 발표하는날이었어요~! 과연 어떤팀이 1등을 했을까요? 두구두구두구 &lt;br /&gt;&lt;br /&gt;정말 쟁쟁한 서비스들이 많이 나왔는데요, 매쉬업은 기수가 늘어날수록 정말 만들어지는 서비스의 퀄리티가 좋아지는게 느껴져요! 구성원들뿐만 아니라 동아리 자체가 함께 앞으로 나아가며 성장하고 있다는 느낌이 듭니다 &lt;br /&gt;&lt;br /&gt;이번 13기가 기록을 갱신한것들이 많지만 대표적인것을 두 가지만 꼽아보자면 매쉬업 창설 이후 최초로 모든 팀 배포 성공이라는 업적을 이루어냈습니다  정말 대단합니다 &lt;br /&gt;나머지 한가지는 역대급 시상 금액입니다! 지난 해커톤때 소개드린 DEVCRA의 후원으로 총 상금 155만원이라는 동아리에서는 정말 큰 금액이 상금으로 주어졌습니다. 1등팀은 무려 70만원의 상금을 받아갔어요  이번에도 감사합니다 DEVCRA!!&lt;br /&gt;&lt;br /&gt;그리고 이번 최종발표도 지난 9차 세미나때처럼 캐치카페에서 공간을 지원주셨는데 아주 쾌적하게 진행 할 수 있도록 해주신 캐치도 감사합니다&amp;zwj;!!️&lt;br /&gt;&lt;br /&gt;마지막으로 3월부터 서비스 MVP 기획, 디자인, 개발하느라 고생하신 Mash-Up 13기 구성원분들도 정말 감사합니다!!&lt;br /&gt;Thank You Mash-Up 13th Crews!!  ️️&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_01.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nQmyI/btssv0zrBZn/SAHP8FkezKNtfAw4QLho70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nQmyI/btssv0zrBZn/SAHP8FkezKNtfAw4QLho70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nQmyI/btssv0zrBZn/SAHP8FkezKNtfAw4QLho70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnQmyI%2Fbtssv0zrBZn%2FSAHP8FkezKNtfAw4QLho70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_01.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_02.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzsvuA/btssBm9H9p4/CPWVWh30K5OCxYPlfdSJG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzsvuA/btssBm9H9p4/CPWVWh30K5OCxYPlfdSJG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzsvuA/btssBm9H9p4/CPWVWh30K5OCxYPlfdSJG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzsvuA%2FbtssBm9H9p4%2FCPWVWh30K5OCxYPlfdSJG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_02.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_03.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P8Rwz/btssDMfOald/AKUn7iMKavI5aqiQtKVu71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P8Rwz/btssDMfOald/AKUn7iMKavI5aqiQtKVu71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P8Rwz/btssDMfOald/AKUn7iMKavI5aqiQtKVu71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP8Rwz%2FbtssDMfOald%2FAKUn7iMKavI5aqiQtKVu71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_03.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_04.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chzoo6/btssCusI8D1/Ix3o1WMHZ16rqsUftpnLQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chzoo6/btssCusI8D1/Ix3o1WMHZ16rqsUftpnLQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chzoo6/btssCusI8D1/Ix3o1WMHZ16rqsUftpnLQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fchzoo6%2FbtssCusI8D1%2FIx3o1WMHZ16rqsUftpnLQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_04.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_05.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4wj2E/btssv0sGBiI/ElwqdypipR11Q9mDsSN81k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4wj2E/btssv0sGBiI/ElwqdypipR11Q9mDsSN81k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4wj2E/btssv0sGBiI/ElwqdypipR11Q9mDsSN81k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4wj2E%2Fbtssv0sGBiI%2FElwqdypipR11Q9mDsSN81k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_05.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_06.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/egr2uX/btssJlhcGlh/VTftwKnwRk9v5bnw3ykKA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/egr2uX/btssJlhcGlh/VTftwKnwRk9v5bnw3ykKA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/egr2uX/btssJlhcGlh/VTftwKnwRk9v5bnw3ykKA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fegr2uX%2FbtssJlhcGlh%2FVTftwKnwRk9v5bnw3ykKA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_06.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_07.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgKYDB/btssxFIMiIL/9NF9Qd6klRGcEYvg0kmAtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgKYDB/btssxFIMiIL/9NF9Qd6klRGcEYvg0kmAtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgKYDB/btssxFIMiIL/9NF9Qd6klRGcEYvg0kmAtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgKYDB%2FbtssxFIMiIL%2F9NF9Qd6klRGcEYvg0kmAtk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_07.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_08.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uneAu/btssBrJMJWO/akyv2PhI4ljRo2OaeF2wl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uneAu/btssBrJMJWO/akyv2PhI4ljRo2OaeF2wl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uneAu/btssBrJMJWO/akyv2PhI4ljRo2OaeF2wl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuneAu%2FbtssBrJMJWO%2Fakyv2PhI4ljRo2OaeF2wl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_08.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_09.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqC4iD/btssHB5FxDw/gQ5KVF2gkuejBK77sqOSN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqC4iD/btssHB5FxDw/gQ5KVF2gkuejBK77sqOSN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqC4iD/btssHB5FxDw/gQ5KVF2gkuejBK77sqOSN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqC4iD%2FbtssHB5FxDw%2FgQ5KVF2gkuejBK77sqOSN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_09.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_10.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eqTA9B/btssBoGsLKP/QO1yjMMdBbSlEkVEPVFCk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eqTA9B/btssBoGsLKP/QO1yjMMdBbSlEkVEPVFCk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eqTA9B/btssBoGsLKP/QO1yjMMdBbSlEkVEPVFCk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeqTA9B%2FbtssBoGsLKP%2FQO1yjMMdBbSlEkVEPVFCk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_10.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>매쉬업 이야기</category>
      <category>it동아리</category>
      <category>mash-up</category>
      <category>개발</category>
      <category>디자인</category>
      <category>매쉬업</category>
      <category>매시업</category>
      <category>캐치</category>
      <category>캐치카페</category>
      <author>MASHUP</author>
      <guid isPermaLink="true">https://mash-up.tistory.com/78</guid>
      <comments>https://mash-up.tistory.com/entry/Mash-Up-13%EA%B8%B0-%EC%B5%9C%EC%A2%85%EB%B0%9C%ED%91%9C#entry78comment</comments>
      <pubDate>Thu, 31 Aug 2023 00:53:28 +0900</pubDate>
    </item>
    <item>
      <title>Mash-Up 13기 9차 세미나</title>
      <link>https://mash-up.tistory.com/entry/Mash-Up-13%EA%B8%B0-9%EC%B0%A8-%EC%84%B8%EB%AF%B8%EB%82%98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;안녕하세요 Mash-Up입니다   7월 15일엔 드디어 해커톤 직전 지금까지 만들어온 프로젝트를 매쉬업 구성원들과 공유하고 점검하는 시간을 가졌습니다 :)&lt;br /&gt;&lt;br /&gt;아직 해커톤 전인데도 정말 톡톡 튀는 아이디어를 가진 쟁쟁한 서비스들을 볼 수 있었던 시간이었어요 &lt;br /&gt;&lt;br /&gt;이번 모임은 매쉬업 후원사인 캐치에서 운영중인 캐치카페 서울대입구점에서 진행되었어요~ 정말 올때마다 쾌적하고 좋은곳이라는 생각이 듭니다  사실 열정만 가지고는 이렇게 모여서 단체 활동을 하며 성장을 도모하는것이 쉽지 않은일인데 IT 동아리, 취준생 등을 위해 후원을 아끼지 않는 캐치 정말 멋있습니다 &lt;br /&gt;&lt;br /&gt;이제 13기가 약 한 달 남았는데 남은 기간동안 매쉬업의 멋진 행보를 지켜봐주세요 &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_01.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TRUje/btssAFaB2mE/h4DeT2ACPXOKAbDosAXd3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TRUje/btssAFaB2mE/h4DeT2ACPXOKAbDosAXd3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TRUje/btssAFaB2mE/h4DeT2ACPXOKAbDosAXd3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTRUje%2FbtssAFaB2mE%2Fh4DeT2ACPXOKAbDosAXd3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_01.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_02.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lmYLW/btssGynQG2a/bMCIHPVp5LUdXdxbtXKKF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lmYLW/btssGynQG2a/bMCIHPVp5LUdXdxbtXKKF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lmYLW/btssGynQG2a/bMCIHPVp5LUdXdxbtXKKF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlmYLW%2FbtssGynQG2a%2FbMCIHPVp5LUdXdxbtXKKF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_02.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_03.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crUvVa/btssHg1F2WU/eHdwbeQCPeotKey9OSgffk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crUvVa/btssHg1F2WU/eHdwbeQCPeotKey9OSgffk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crUvVa/btssHg1F2WU/eHdwbeQCPeotKey9OSgffk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrUvVa%2FbtssHg1F2WU%2FeHdwbeQCPeotKey9OSgffk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_03.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_04.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8xYZh/btsswjMHQ68/o2KOk8LngC8vWFratM5ux1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8xYZh/btsswjMHQ68/o2KOk8LngC8vWFratM5ux1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8xYZh/btsswjMHQ68/o2KOk8LngC8vWFratM5ux1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8xYZh%2FbtsswjMHQ68%2Fo2KOk8LngC8vWFratM5ux1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_04.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_05.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLomX6/btssBnOjYck/lDR1kvL9Drpk8LnMWotfv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLomX6/btssBnOjYck/lDR1kvL9Drpk8LnMWotfv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLomX6/btssBnOjYck/lDR1kvL9Drpk8LnMWotfv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLomX6%2FbtssBnOjYck%2FlDR1kvL9Drpk8LnMWotfv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_05.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_06.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nmmuG/btssv16c7Oa/F9NRj1Xmog2ipGW0cYd8l0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nmmuG/btssv16c7Oa/F9NRj1Xmog2ipGW0cYd8l0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nmmuG/btssv16c7Oa/F9NRj1Xmog2ipGW0cYd8l0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnmmuG%2Fbtssv16c7Oa%2FF9NRj1Xmog2ipGW0cYd8l0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_06.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_07.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5bf7L/btssxGgCIqR/3zyOyVHAUW2ExdXACakbFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5bf7L/btssxGgCIqR/3zyOyVHAUW2ExdXACakbFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5bf7L/btssxGgCIqR/3zyOyVHAUW2ExdXACakbFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5bf7L%2FbtssxGgCIqR%2F3zyOyVHAUW2ExdXACakbFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_07.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_08.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0TW54/btssHfoc5X1/u9ktEpbZG9wCDeBUVHczl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0TW54/btssHfoc5X1/u9ktEpbZG9wCDeBUVHczl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0TW54/btssHfoc5X1/u9ktEpbZG9wCDeBUVHczl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0TW54%2FbtssHfoc5X1%2Fu9ktEpbZG9wCDeBUVHczl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2160&quot; height=&quot;2160&quot; data-filename=&quot;Instagram_08.png&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2160&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;</description>
      <category>매쉬업 이야기</category>
      <category>it동아리</category>
      <category>mash-up</category>
      <category>개발</category>
      <category>디자인</category>
      <category>매쉬업</category>
      <category>캐치</category>
      <category>캐치카페</category>
      <author>MASHUP</author>
      <guid isPermaLink="true">https://mash-up.tistory.com/77</guid>
      <comments>https://mash-up.tistory.com/entry/Mash-Up-13%EA%B8%B0-9%EC%B0%A8-%EC%84%B8%EB%AF%B8%EB%82%98#entry77comment</comments>
      <pubDate>Thu, 31 Aug 2023 00:51:53 +0900</pubDate>
    </item>
    <item>
      <title>Mash-Up 13기 플랫폼 작업모임</title>
      <link>https://mash-up.tistory.com/entry/Mash-Up-13%EA%B8%B0-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EC%9E%91%EC%97%85%EB%AA%A8%EC%9E%84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;안녕하세요 Mash-Up입니다   6월 3일에는 Mash-Up 플랫폼 작업모임이 진행되었어요~ 프로젝트의 박차를 가하기 위해서 낮부터 모여서 하루종일 작업을 진행했답니다~ &lt;br /&gt;&lt;br /&gt;장소는 캐치카페에서 대관을 지원해주셨습니다~ 너무 넓고 쾌적했어요~ 게대가 1인 1음료까지 제공되니 너무 좋은 장소 같아요:)&lt;br /&gt;대학생 혹은 취준생이라면 누구나 무료로 방문할 수 있고 음료도 제공받을 수 있다고 하니, 대학생 혹은 취준생이라면 혜택을 꼭 받아보세요:)&lt;br /&gt;&lt;br /&gt;오늘의 모임을 통해서 작업이 많이 진행된 것 같은데요 ㅎㅎ 이번 기수 탄생할 서비스도 매우 기대가 됩니다:)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_01.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oD20R/btskEWCVNia/ZPlX0Qe5qAAHyUulOldsj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oD20R/btskEWCVNia/ZPlX0Qe5qAAHyUulOldsj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oD20R/btskEWCVNia/ZPlX0Qe5qAAHyUulOldsj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoD20R%2FbtskEWCVNia%2FZPlX0Qe5qAAHyUulOldsj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot; data-filename=&quot;Instagram_01.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_02.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8URrk/btskAERoFRi/3hjKrWP2m6YG0oSczZb240/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8URrk/btskAERoFRi/3hjKrWP2m6YG0oSczZb240/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8URrk/btskAERoFRi/3hjKrWP2m6YG0oSczZb240/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8URrk%2FbtskAERoFRi%2F3hjKrWP2m6YG0oSczZb240%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot; data-filename=&quot;Instagram_02.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_03.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mL7q6/btskyoBg1uJ/GlYpCv8kzSVBZHpCCleZUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mL7q6/btskyoBg1uJ/GlYpCv8kzSVBZHpCCleZUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mL7q6/btskyoBg1uJ/GlYpCv8kzSVBZHpCCleZUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmL7q6%2FbtskyoBg1uJ%2FGlYpCv8kzSVBZHpCCleZUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot; data-filename=&quot;Instagram_03.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_04.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R60iX/btskEVRyu5V/AOyKkP7wgdljNamYBKObQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R60iX/btskEVRyu5V/AOyKkP7wgdljNamYBKObQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R60iX/btskEVRyu5V/AOyKkP7wgdljNamYBKObQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR60iX%2FbtskEVRyu5V%2FAOyKkP7wgdljNamYBKObQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot; data-filename=&quot;Instagram_04.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_05.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2VdId/btskrbWNEQi/UZTHUZsemkrksivlPVDMKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2VdId/btskrbWNEQi/UZTHUZsemkrksivlPVDMKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2VdId/btskrbWNEQi/UZTHUZsemkrksivlPVDMKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2VdId%2FbtskrbWNEQi%2FUZTHUZsemkrksivlPVDMKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot; data-filename=&quot;Instagram_05.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_06.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1KPZV/btskynoPsHb/FexE6kn59qtGUqXtp82eLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1KPZV/btskynoPsHb/FexE6kn59qtGUqXtp82eLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1KPZV/btskynoPsHb/FexE6kn59qtGUqXtp82eLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1KPZV%2FbtskynoPsHb%2FFexE6kn59qtGUqXtp82eLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot; data-filename=&quot;Instagram_06.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Instagram_07.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dy64rd/btskCoGMsR4/RDvLdvkhbZkFmZDvhkbuK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dy64rd/btskCoGMsR4/RDvLdvkhbZkFmZDvhkbuK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dy64rd/btskCoGMsR4/RDvLdvkhbZkFmZDvhkbuK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdy64rd%2FbtskCoGMsR4%2FRDvLdvkhbZkFmZDvhkbuK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot; data-filename=&quot;Instagram_07.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&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;</description>
      <category>대학생</category>
      <category>무료대관</category>
      <category>서울대점</category>
      <category>취준생</category>
      <category>카페추천</category>
      <category>캐치카페</category>
      <author>MASHUP</author>
      <guid isPermaLink="true">https://mash-up.tistory.com/76</guid>
      <comments>https://mash-up.tistory.com/entry/Mash-Up-13%EA%B8%B0-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EC%9E%91%EC%97%85%EB%AA%A8%EC%9E%84#entry76comment</comments>
      <pubDate>Tue, 20 Jun 2023 00:00:47 +0900</pubDate>
    </item>
    <item>
      <title>HMM팀의 취문취답 서비스, 친친을 소개합니다 </title>
      <link>https://mash-up.tistory.com/entry/HMM%ED%8C%80%EC%9D%98-%EC%B7%A8%EB%AC%B8%EC%B7%A8%EB%8B%B5-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%B9%9C%EC%B9%9C%EC%9D%84-%EC%86%8C%EA%B0%9C%ED%95%A9%EB%8B%88%EB%8B%A4%F0%9F%A5%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5760&quot; data-origin-height=&quot;3900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EWZmn/btrNuUFO7nF/Oad9KpYnXMP0xiXgXjj4iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EWZmn/btrNuUFO7nF/Oad9KpYnXMP0xiXgXjj4iK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EWZmn/btrNuUFO7nF/Oad9KpYnXMP0xiXgXjj4iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEWZmn%2FbtrNuUFO7nF%2FOad9KpYnXMP0xiXgXjj4iK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;5760&quot; height=&quot;3900&quot; data-origin-width=&quot;5760&quot; data-origin-height=&quot;3900&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;안드로이드 팀에서 10기부터 활동중인 유희진입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이번 12기에 HMM팀에 속해서 &lt;b&gt;&lt;i&gt;취향으로 묻고 취향으로 답하는 취문 취답&lt;/i&gt; &lt;i&gt;서비스&lt;/i&gt;&lt;/b&gt; &lt;b&gt;&lt;u&gt;친친&lt;/u&gt;!!&lt;/b&gt;을 만들었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.62em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;친친 서비스에 대해서..&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분들은 평소에 가깝게 지내는 지인들의 취향을 얼마나 알고계신가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 아 매쉬업에서 만난 혜진언니가 이번에 생일인데.. 분명히 어떤 와인 종류 좋아하는지 얘기했던것같은데...!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 대리님이 곧 출산예정일인데 아기 선물은 뭐해줘야하지? 아가 이름을 들었던것같은데....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명히 들었던 것 같은 그들의 취향, 하지만 꼭 선물을 줘야할 시기엔 가물가물해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네. 친친은 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;&quot;내 주변에 꼭 함께 하고싶은 내 바운더리의 사람들을 잘 관리하고 싶다&quot;&lt;/b&gt; &lt;/span&gt;에서 시작했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어렵게만 느껴졌던 인맥관리를 조금 더 재미있게 취문취답으로 풀어보고 싶었답니다 :)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 세 가지 니즈가 있으시다면 친친이 필요한 분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 서로의 취향을 부담없이 물어보고 싶은 사람&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;3. 지인의 경조사에 어떤 선물을 해야할지 고민인 사람&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;친친's Solution&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;질문을 통해 알아가보세요!&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 물어보기 어려웠던 질문들을 취향 질문지를 만들어 보내보세요! 그 과정에서 내가 예상 답변을 작성할수도 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가물가물한 친구가 좋아하는 향수 향을 한 번 물어봐볼까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;Q: 희진이가 좋아하는 향수 브랜드는? 그리고 향은?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;A: 음... 내가 들었던건 산타마리아노벨라 프리지아인데 꽃향기 좋아했던것같아!!! 맞나?&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;u&gt;여기서 꿀팁을 하나 드리면요, 싫어하는걸 질문지로 구성하는 것도 방법&lt;/u&gt;이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 친구 선물로 꼭 그건 피할 수 있으니까요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;Q: 희진이가 싫어하는 향수 향은?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;A: 머스크향은 머리가 아팠다고 했던 것 같아!! 맞나?&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;/p&gt;
&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: #ef6f53;&quot;&gt;&lt;b&gt;Q: 희진이가 좋아하는 향수 브랜드는? 그리고 향은?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;&lt;b&gt;A: 산타마리아노벨라 프리지아! 꽃향기 좋아해! :)&amp;nbsp;&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: #ef6f53;&quot;&gt;&lt;b&gt;Q. 희진이가 싫어하는 향수향은?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;&lt;b&gt;A: 머스크향! 머리아프기도하고 나랑 안어울려.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 저희 앱의 취문취답 플로우 아래 이미지에서 확인해보시면 좋을 것 같아요  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-30 오후 9.55.17.png&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beOuky/btrNwnfDCW4/U4dSOy2dRh3T0EyJ5Cx45k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beOuky/btrNwnfDCW4/U4dSOy2dRh3T0EyJ5Cx45k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beOuky/btrNwnfDCW4/U4dSOy2dRh3T0EyJ5Cx45k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeOuky%2FbtrNwnfDCW4%2FU4dSOy2dRh3T0EyJ5Cx45k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1110&quot; height=&quot;1166&quot; data-filename=&quot;스크린샷 2022-09-30 오후 9.55.17.png&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;1166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-30 오후 9.55.25.png&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;998&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDmxQj/btrNuTNCChd/iezuCQHTHnRb2I4uk3t1U0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDmxQj/btrNuTNCChd/iezuCQHTHnRb2I4uk3t1U0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDmxQj/btrNuTNCChd/iezuCQHTHnRb2I4uk3t1U0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDmxQj%2FbtrNuTNCChd%2FiezuCQHTHnRb2I4uk3t1U0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1088&quot; height=&quot;998&quot; data-filename=&quot;스크린샷 2022-09-30 오후 9.55.25.png&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;998&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;서로의 답변을 비교해봐요!&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 한 가지 재밌는 부분은! 나의 예상 답변과 친구의 답변을 비교해볼 수 있다는 점이에요~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친구와의 티키타카 한 번 확인해보면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;아 맞닼ㅋㅋㅋㅋㅋㅋㅋ&quot; 하면서 기억도 되살리고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;뭐야 이걸 싫어한다고? 이건 못참는데&quot; 하면서 &lt;s&gt;친구 관계를 다시한번 고민&amp;nbsp;&lt;/s&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;5760&quot; data-origin-height=&quot;3517&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vlbKV/btrNu6MJlIx/PQ3S5NK12d222Se9KA9xyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vlbKV/btrNu6MJlIx/PQ3S5NK12d222Se9KA9xyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vlbKV/btrNu6MJlIx/PQ3S5NK12d222Se9KA9xyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvlbKV%2FbtrNu6MJlIx%2FPQ3S5NK12d222Se9KA9xyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;5760&quot; height=&quot;3517&quot; data-origin-width=&quot;5760&quot; data-origin-height=&quot;3517&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;그룹핑과 Easy한 친구 등록&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 기능이 될 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 인맥관리는 그룹핑이 필요할 것 같아서 제공하게 되었습니다. 앱의 가장 메인에서 그룹들을 확인하고, 친구를 추가할 수 있어요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-30 오후 10.08.41.png&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;1718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHFgUk/btrNv9hEvuM/jnp4Rv46oTbw9zkXQyqrXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHFgUk/btrNv9hEvuM/jnp4Rv46oTbw9zkXQyqrXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHFgUk/btrNv9hEvuM/jnp4Rv46oTbw9zkXQyqrXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHFgUk%2FbtrNv9hEvuM%2Fjnp4Rv46oTbw9zkXQyqrXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1412&quot; height=&quot;1718&quot; data-filename=&quot;스크린샷 2022-09-30 오후 10.08.41.png&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;1718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친구는 친친을 사용하지 않는 미가입자 친구도 기록용으로 등록할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후에 카카오톡 친구 찾기 권한동의만 한다면 손쉽게 카카오톡의 친구 목록을 불러오고 친구를 추가할 수 있어요! 일일이 친구를 등록하는것에 어려움을 겪기 싫어서 추가한 기획입니다 :)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-30 오후 10.13.43.png&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmLQm2/btrNuCk5JBS/uWeqSO0vKWzqWiiCWsXlUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmLQm2/btrNuCk5JBS/uWeqSO0vKWzqWiiCWsXlUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmLQm2/btrNuCk5JBS/uWeqSO0vKWzqWiiCWsXlUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmLQm2%2FbtrNuCk5JBS%2FuWeqSO0vKWzqWiiCWsXlUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1512&quot; height=&quot;914&quot; data-filename=&quot;스크린샷 2022-09-30 오후 10.13.43.png&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;914&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;h3 data-ke-size=&quot;size23&quot;&gt;귀염둥이 친친바라&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;1003&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6jVD3/btrNvxpOkI5/OPbfLOLjIKqBSzMhhmHtT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6jVD3/btrNvxpOkI5/OPbfLOLjIKqBSzMhhmHtT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6jVD3/btrNvxpOkI5/OPbfLOLjIKqBSzMhhmHtT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6jVD3%2FbtrNvxpOkI5%2FOPbfLOLjIKqBSzMhhmHtT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;306&quot; height=&quot;220&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;1003&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분... 그런데 꼭 눈에띄는 캐릭터가 하나 있어요...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로바로 요 귀염둥이 친친바라에요~~!! 뀨 ㅇㅅㅇ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&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 floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZKc9C/btrNwEO0KiL/Wqbv9arSnMTvKSpiRH6P41/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZKc9C/btrNwEO0KiL/Wqbv9arSnMTvKSpiRH6P41/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZKc9C/btrNwEO0KiL/Wqbv9arSnMTvKSpiRH6P41/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZKc9C%2FbtrNwEO0KiL%2FWqbv9arSnMTvKSpiRH6P41%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;330&quot; height=&quot;264&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;599&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저희 친친서비스의 캐릭터로 해야겠다 마음을 먹었어요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 예상치 못한 난관이 있었으니...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카피바라의 정면모습이 생각보다 별 특징이 없다는 점... 흠....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 저희 디자인팀은 바로 능력자들이었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기서 특징을 열심히 집어내서!!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 많은 스케치 과정을 거쳐~~~&lt;s&gt;(저도 한 번 그려봤었답니다 ㅋ.ㅋ 참고로 전 똥손 안드로이드개발자)&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;귀염둥이 친친바라가 만들어졌어요~!! 짱귀엽죠? 저희 발표날에 사실 서비스보다 친친바라 칭찬이 더 많았음..ㅎ; 서비스도 알아주라구..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-30 오후 10.18.21.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QFiFv/btrNuVko1YE/D8W3WUWcjdohtoZVaQuka1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QFiFv/btrNuVko1YE/D8W3WUWcjdohtoZVaQuka1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QFiFv/btrNuVko1YE/D8W3WUWcjdohtoZVaQuka1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQFiFv%2FbtrNuVko1YE%2FD8W3WUWcjdohtoZVaQuka1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1448&quot; height=&quot;1532&quot; data-filename=&quot;스크린샷 2022-09-30 오후 10.18.21.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꺄 !! 서비스 소개 끝이에요! 어떠세요!!!?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들면서 내가 꼭 써봐야지! 재밌겠다! 하면서 만들었답니다! :)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분도 꼭 친친 사용해보세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아 맞다! 친친은 안드로이드만 제공되고 있어요 ㅠ_ㅠ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래서 다운받아 사용해보세요 !! XD&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://play.google.com/store/apps/details?id=com.mashup.chinchin&quot;&gt;https://play.google.com/store/apps/details?id=com.mashup.chinchin&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1664543793836&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 Play 앱&quot; data-og-description=&quot;친한 친구의 취향을 알아보자&quot; data-og-host=&quot;play.google.com&quot; data-og-source-url=&quot;https://play.google.com/store/apps/details?id=com.mashup.chinchin&quot; data-og-url=&quot;https://play.google.com/store/apps/details?id=com.mashup.chinchin&amp;amp;hl=ko&amp;amp;gl=JP&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bmqrHz/hyPY8lrjiN/mtxbnMQVZEXGekmQdUKPuK/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/bVO5MF/hyPY95I6By/a9ZLKJc66AKfRekkyhhbH0/img.png?width=600&amp;amp;height=300&amp;amp;face=0_0_600_300,https://scrap.kakaocdn.net/dn/dr0i0B/hyPXNwq5NV/74pI08s7SKONb7ZKayw1cK/img.png?width=240&amp;amp;height=240&amp;amp;face=0_0_240_240&quot;&gt;&lt;a href=&quot;https://play.google.com/store/apps/details?id=com.mashup.chinchin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://play.google.com/store/apps/details?id=com.mashup.chinchin&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bmqrHz/hyPY8lrjiN/mtxbnMQVZEXGekmQdUKPuK/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/bVO5MF/hyPY95I6By/a9ZLKJc66AKfRekkyhhbH0/img.png?width=600&amp;amp;height=300&amp;amp;face=0_0_600_300,https://scrap.kakaocdn.net/dn/dr0i0B/hyPXNwq5NV/74pI08s7SKONb7ZKayw1cK/img.png?width=240&amp;amp;height=240&amp;amp;face=0_0_240_240');&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 Play 앱&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;친한 친구의 취향을 알아보자&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;play.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;즐거웠던 HMM모먼트 몇 개만 보여드리면서 마무리 하도록 할게요~ &lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1081&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k6y98/btrNwoZXAhQ/IRsm7bIT5rDYESLPs3rLhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k6y98/btrNwoZXAhQ/IRsm7bIT5rDYESLPs3rLhK/img.png&quot; data-alt=&quot;기획 열심히 하는 것 처럼 보이지만 2차해커톤 방잡는중.. 아니 이것도 열심히하긴했어..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k6y98/btrNwoZXAhQ/IRsm7bIT5rDYESLPs3rLhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk6y98%2FbtrNwoZXAhQ%2FIRsm7bIT5rDYESLPs3rLhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;351&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1081&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기획 열심히 하는 것 처럼 보이지만 2차해커톤 방잡는중.. 아니 이것도 열심히하긴했어..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1081&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqCLrY/btrNuMnLcqi/zgOQjPhyZpL92sVwkpofO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqCLrY/btrNuMnLcqi/zgOQjPhyZpL92sVwkpofO1/img.png&quot; data-alt=&quot;다들 열일중~ 머쪄 모야모야~!!!!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqCLrY/btrNuMnLcqi/zgOQjPhyZpL92sVwkpofO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqCLrY%2FbtrNuMnLcqi%2FzgOQjPhyZpL92sVwkpofO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;413&quot; height=&quot;310&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1081&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다들 열일중~ 머쪄 모야모야~!!!!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xHWXJ/btrNwoeBcnu/BY51qXBVPnX9CrQVhDheU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xHWXJ/btrNwoeBcnu/BY51qXBVPnX9CrQVhDheU1/img.png&quot; data-alt=&quot;연중팀장님 손가락 덕분에 우리팀 노래맞추기 1등했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xHWXJ/btrNwoeBcnu/BY51qXBVPnX9CrQVhDheU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxHWXJ%2FbtrNwoeBcnu%2FBY51qXBVPnX9CrQVhDheU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;403&quot; height=&quot;537&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;연중팀장님 손가락 덕분에 우리팀 노래맞추기 1등했다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XvGzd/btrNviNamz9/oKvCzbrTSGC4mAhl87vGf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XvGzd/btrNviNamz9/oKvCzbrTSGC4mAhl87vGf0/img.png&quot; data-alt=&quot;귀여워ㅡㅡ!!!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XvGzd/btrNviNamz9/oKvCzbrTSGC4mAhl87vGf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXvGzd%2FbtrNviNamz9%2FoKvCzbrTSGC4mAhl87vGf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;421&quot; height=&quot;562&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;귀여워ㅡㅡ!!!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RKvbj/btrNuR3qEPF/GxosX9oCUg9OUz7UsCQBN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RKvbj/btrNuR3qEPF/GxosX9oCUg9OUz7UsCQBN1/img.png&quot; data-alt=&quot;UI그만.. api 붙이고싶어요.. 나 거북목왤케심해..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RKvbj/btrNuR3qEPF/GxosX9oCUg9OUz7UsCQBN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRKvbj%2FbtrNuR3qEPF%2FGxosX9oCUg9OUz7UsCQBN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;330&quot; height=&quot;440&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;UI그만.. api 붙이고싶어요.. 나 거북목왤케심해..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/osh2D/btrNwn01Vts/8T9TRrfm7NqZY9KOSdMdwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/osh2D/btrNwn01Vts/8T9TRrfm7NqZY9KOSdMdwk/img.png&quot; data-alt=&quot;3차 해커톤 밤 산책~&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/osh2D/btrNwn01Vts/8T9TRrfm7NqZY9KOSdMdwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fosh2D%2FbtrNwn01Vts%2F8T9TRrfm7NqZY9KOSdMdwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;418&quot; height=&quot;235&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;3차 해커톤 밤 산책~&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1081&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/plKEN/btrNwjxAmbJ/PKUHK8JOrioiJbVjAypTb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/plKEN/btrNwjxAmbJ/PKUHK8JOrioiJbVjAypTb0/img.png&quot; data-alt=&quot;피자 냠냐미~ 안먹는다해놓고 계속먹는중~ 으휴~&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/plKEN/btrNwjxAmbJ/PKUHK8JOrioiJbVjAypTb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FplKEN%2FbtrNwjxAmbJ%2FPKUHK8JOrioiJbVjAypTb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;307&quot; height=&quot;230&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1081&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;피자 냠냐미~ 안먹는다해놓고 계속먹는중~ 으휴~&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1081&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuZadp/btrNvP4PRR5/fXjfgRZY8wQ6HnGXN6igbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuZadp/btrNvP4PRR5/fXjfgRZY8wQ6HnGXN6igbK/img.png&quot; data-alt=&quot;3차 해커톤 후 꼬질꼬질 상태..는 작게..~&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuZadp/btrNvP4PRR5/fXjfgRZY8wQ6HnGXN6igbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuZadp%2FbtrNvP4PRR5%2FfXjfgRZY8wQ6HnGXN6igbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;184&quot; height=&quot;138&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1081&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;3차 해커톤 후 꼬질꼬질 상태..는 작게..~&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그럼 안뇽~~~~~~~~~~~~~~~~~~~~~~  &lt;/b&gt;&lt;/p&gt;</description>
      <category>프로젝트 이야기</category>
      <category>12기프로젝트</category>
      <category>hmm</category>
      <category>매쉬업</category>
      <category>친친</category>
      <author>huijiny</author>
      <guid isPermaLink="true">https://mash-up.tistory.com/75</guid>
      <comments>https://mash-up.tistory.com/entry/HMM%ED%8C%80%EC%9D%98-%EC%B7%A8%EB%AC%B8%EC%B7%A8%EB%8B%B5-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%B9%9C%EC%B9%9C%EC%9D%84-%EC%86%8C%EA%B0%9C%ED%95%A9%EB%8B%88%EB%8B%A4%F0%9F%A5%B0#entry75comment</comments>
      <pubDate>Fri, 30 Sep 2022 22:42:53 +0900</pubDate>
    </item>
    <item>
      <title>[스프링팀] 검색 기술의 원리 발표 세션</title>
      <link>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%8C%80-%EA%B2%80%EC%83%89-%EA%B8%B0%EC%88%A0%EC%9D%98-%EC%9B%90%EB%A6%AC-%EB%B0%9C%ED%91%9C-%EC%84%B8%EC%85%98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요, 스프링팀 김경환입니다. 지난 6월 11일 삼성역 하이퍼커넥트 사옥에서 매시업 전체 5차 세미나를 진행했습니다! &lt;br&gt;&lt;br&gt;매시업 전체 세미나에서는 매 기수마다 모든 팀이 각각 두 세션 씩 주제를 정해서 발표를 하는데요, &lt;br&gt;&lt;br&gt;12기 스프링팀에서는 개발자의 사실과 오해를 다룬 개발자 밈을 소개하는 세션과, 검색 기술의 원리를 소개하는 세션을 진행했습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lw3bG/btrMafiWtVc/BRX3pNqa8TR9DJVhKHvPWk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lw3bG/btrMafiWtVc/BRX3pNqa8TR9DJVhKHvPWk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lw3bG/btrMafiWtVc/BRX3pNqa8TR9DJVhKHvPWk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLw3bG%2FbtrMafiWtVc%2FBRX3pNqa8TR9DJVhKHvPWk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;699&quot; height=&quot;524&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;이번 세미나에서는 약 100명에 가까운 매시업 크루원분들이 참석해주셔서 그 어느때보다 더 활기찼는데요! 스프링팀 발표 세션에 뜨거운 호응 보내주신 점 감사드립니다! &lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;611&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VP0P4/btrVE2nXq38/2ekDcwOTwtxXLTvWtVIFKk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VP0P4/btrVE2nXq38/2ekDcwOTwtxXLTvWtVIFKk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VP0P4/btrVE2nXq38/2ekDcwOTwtxXLTvWtVIFKk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVP0P4%2FbtrVE2nXq38%2F2ekDcwOTwtxXLTvWtVIFKk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;611&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;611&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&gt;스프링팀 세션 중 제가 발표한 검색 기술의 원리를 소개하는 세션 내용을 정리해서 공유하고자 합니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 검색이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTZUmv/btrL8ZnTJiX/HKyhueXn8TTLqYtlcsA0n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTZUmv/btrL8ZnTJiX/HKyhueXn8TTLqYtlcsA0n0/img.png&quot; data-alt=&quot; 이미지출처:https://www.overpopulationatlas.com/post/where-s-wally &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTZUmv/btrL8ZnTJiX/HKyhueXn8TTLqYtlcsA0n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTZUmv%2FbtrL8ZnTJiX%2FHKyhueXn8TTLqYtlcsA0n0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;744&quot; height=&quot;417&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 이미지출처:https://www.overpopulationatlas.com/post/where-s-wally &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&gt;검색 기술을 소개하기 앞서, 윌리를 같이 찾아볼까요?&lt;br&gt;&lt;br&gt;&quot;윌리를 찾아라&quot;는 수많은 사람들로 가득찬 큰 그림 속에서 청바지에 빨간색 줄무늬 스웨터와 모자를 쓴 윌리를 찾는 게임입니다!&lt;br&gt;&lt;br&gt;여러분은 윌리를 찾을 때 어디서부터 찾으시나요?&lt;br&gt;&lt;br&gt;눈을 가까이 대고 위에서부터 아래로, 혹은 왼쪽에서 오른쪽으로 한명 한명 들여다보며 찾기도 하고, 멀리 떨어져서 윌리가 있을 법한 장소를 물색해 그 부분을 자세히 들여다 보기도 합니다.&lt;br&gt;&lt;br&gt;큰 그림 속 수많은 사람들 중에서 윌리를 찾는 일은 쉬운 일이 아닌데요, 혹시 찾으셨나요? &lt;br&gt;&lt;br&gt;실제로 세미나에서 100명에 가까운 매시업 크루들이 같이 찾아봤지만 윌리를 찾는데는 꽤 오랜 시간이 걸렸습니다.&lt;br&gt;&lt;br&gt;윌리와 상관 없는 많은 사람들 속에서 우리가 원하는 사람인 윌리를 찾는 것과 검색은 꽤나 유사한 면이 있는데요,&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7zY2c/btrL5xlj95e/XqmdM2kBBBSJv9NosFM820/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7zY2c/btrL5xlj95e/XqmdM2kBBBSJv9NosFM820/img.jpg&quot; data-alt=&quot; 맛있는 식당을 찾기란 쉽지 않다 &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7zY2c/btrL5xlj95e/XqmdM2kBBBSJv9NosFM820/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7zY2c%2FbtrL5xlj95e%2FXqmdM2kBBBSJv9NosFM820%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;417&quot; height=&quot;442&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 맛있는 식당을 찾기란 쉽지 않다 &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&gt;우리는 흔히 처음 가는 장소를 찾을 때나 맛집을 찾을 때 검색합니다. 가야할 장소와 상관 없는 수많은 건물 중 우리가 원하는 건물이 어디있는 지 검색하고, 수많은 음식점과 카페 중 맛집만을 가려내기 위해 검색합니다. 이처럼 검색이란 &lt;b&gt;&quot;많은 정보 중 원하는 정보만 찾아내는 것&quot;&lt;/b&gt;으로 정의할 수 있습니다. 많은 사람들 중 윌리를 찾고, 많은 음식점 중 맛집만을 찾는것처럼요.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 그렇다면 검색을 어떻게 해야하지?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;수많은 정보 중에서 우리가 원하는 정보를 어떻게 찾을 수 있을까요? &lt;br&gt;&lt;br&gt;간단한 예시를 들어봅시다. 매시업 전체 5차 세미나는 삼성역 아셈타워에 있는 하이퍼커넥트에서 진행됐는데요,&lt;br&gt;&lt;br&gt;하이퍼커넥트에 오기 위해 수많은 건물 중 아셈타워를 어떻게 찾을 수 있을까요? &lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWXe8T/btrL9j0IJhC/p4WRVGzQA3RS0iI4QXRAEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWXe8T/btrL9j0IJhC/p4WRVGzQA3RS0iI4QXRAEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWXe8T/btrL9j0IJhC/p4WRVGzQA3RS0iI4QXRAEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWXe8T%2FbtrL9j0IJhC%2Fp4WRVGzQA3RS0iI4QXRAEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;673&quot; height=&quot;420&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;아셈타워를 찾기 위해서는 많은 건물 이름 중 &quot;아셈&quot;이라는 글자가 들어간 건물을 찾아야 합니다. &lt;br&gt;&lt;br&gt;컴퓨터는 아셈을 찾기 위해 &lt;b&gt;하나하나씩 비교&lt;/b&gt;를 해볼 수 밖에 없습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpfI7F/btrL8wTLCWS/C01rKiwP8k5PLu62bFZDbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpfI7F/btrL8wTLCWS/C01rKiwP8k5PLu62bFZDbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpfI7F/btrL8wTLCWS/C01rKiwP8k5PLu62bFZDbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpfI7F%2FbtrL8wTLCWS%2FC01rKiwP8k5PLu62bFZDbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;694&quot; height=&quot;467&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&gt;먼저 &quot;남산&quot;과 &quot;아셈&quot;이 일치하는지 확인하고, 다음 두글자인 &quot;산전&quot;과 &quot;아셈&quot;이 일치하는 지 확인하는 과정을 반복해 아셈타워를 찾을 수 있습니다. &lt;br&gt;&lt;br&gt;이번 예제에서는 “남산”, “산전”, “전망”, “망대”, “아셈” 순으로 총 5번의 비교연산을 통해 아셈타워를 찾을 수 있겠지만 실제로 적용이 가능할까요?&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/obblt/btrMabnk1eK/zdpVQKHlFl5JEu1bQQ2Fb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/obblt/btrMabnk1eK/zdpVQKHlFl5JEu1bQQ2Fb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/obblt/btrMabnk1eK/zdpVQKHlFl5JEu1bQQ2Fb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fobblt%2FbtrMabnk1eK%2FzdpVQKHlFl5JEu1bQQ2Fb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;734&quot; height=&quot;417&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;만약 건물 개수가 1000만개라면 최소 1000만번 이상의 비교 연산을 수행해야 하고, 건물 이름이 길다면 그 길이의 배수만큼 더 연산이 필요할 것입니다. 문자열 매칭 알고리즘을 사용한다면 (건물이름의 평균 길이 * 건물 개수) 만큼의 시간이 필요합니다.&lt;br&gt;&lt;br&gt;매시업 세미나에 가기 위해 아셈타워를 검색하더라도 검색 결과가 나온 시간이 세미나가 끝난 시간이라면 검색은 의미가 없겠죠.&lt;br&gt;&lt;br&gt;이처럼 &lt;b&gt;검색은 데이터를 조회할 때 많은 연산&lt;/b&gt;이 필요합니다.&lt;br&gt;&lt;br&gt;그렇다면 검색할 때마다 찾지 않고 &lt;b&gt;데이터를 쓸 때 미리 찾아놓는 것&lt;/b&gt;은 어떨까요?&lt;br&gt;&lt;br&gt;“검색될 데이터를 미리 찾아놓는다”는 말이 다소 모순적으로 들리는데요, 물론 미래에 사용자가 어떤 키워드로 검색을 할 지는 모릅니다!&lt;br&gt;&lt;br&gt;대신 &lt;b&gt;“검색이 될만한 문자열”&lt;/b&gt;로 미리 분리해놓을 수는 있습니다. 앞서 살펴본 예제에서 “남산전망대”라는 키워드에 “아셈”이 들어있는 지 확인할 때 “산전”, “망대”와 같이 의미없는 부분 문자열과도 비교 연산을 수행하는 것을 볼 수 있었습니다. “남산전망대” 라는 단어는 “남산” 과 “전망대”라는 명사가 합쳐진 단어입니다. 따라서 사용자는 “남산전망대”를 찾기 위해 주로 “남산” 혹은 “전망대”라는 키워드로 검색할 것이라고 추론할 수 있습니다. 이제 “남산전망대”라는 단어를 “남산”과 “전망대”라는 단어로 분리하여 각각 저장하고, 각 단어들이 “남산전망대”라는 &lt;b&gt;원본 문서를 가르키도록&lt;/b&gt; 하면 어떨까요?&lt;br&gt;&lt;br&gt;이처럼 &lt;b&gt;데이터를 찾기 위해 추가적인 자료구조를 사용하는 것을 색인&lt;/b&gt;이라고 합니다.&lt;br&gt;&lt;br&gt;검색에서 사용되는 색인은 데이터의 식별자를 사용하여 데이터를 찾는 일반적인 색인과 달리 &lt;b&gt;데이터를 사용하여 데이터의 식별자를 찾는, 반대로된 색인&lt;/b&gt;이기 때문에 &lt;b&gt;역색인&lt;/b&gt;(Inverted Index)라고 부릅니다.&lt;br&gt;&lt;br&gt;앞선 예제를 역색인 구조로 나타낸 표는 다음과 같습니다. “아셈타워”가 “아셈”과 “타워”로, “롯데월드타워”는 “롯데월드”와 “타워”로 분리된 모습을 볼 수 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2256&quot; data-origin-height=&quot;1282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buJJby/btrMabOohXC/srAZy1KkFGDYqA9AfhqZxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buJJby/btrMabOohXC/srAZy1KkFGDYqA9AfhqZxk/img.png&quot; data-alt=&quot; 좌측:역색인 구조 우측:원본 문서 &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buJJby/btrMabOohXC/srAZy1KkFGDYqA9AfhqZxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuJJby%2FbtrMabOohXC%2FsrAZy1KkFGDYqA9AfhqZxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2256&quot; height=&quot;1282&quot; data-origin-width=&quot;2256&quot; data-origin-height=&quot;1282&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 좌측:역색인 구조 우측:원본 문서 &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;마찬가지로 검색어를 사용해 검색이 될만한 문자열을 찾아야 하기 때문에 검색어 또한 검색이 될만한 문자열로 분리하는 과정이 필요합니다. &lt;br&gt;&lt;br&gt;예를 들어 “포스코타워”로 검색한다면 “포스코” 와 “타워”로 검색어가 분리되고, “타워”라는 문자열과 일치해 검색어와 비슷한 “롯데월드타워”와 “아셈타워”를 찾을 수 있습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2274&quot; data-origin-height=&quot;1300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTbDAX/btrL9ZgmOTt/V3CDzi6FP4fOpLukQg9h81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTbDAX/btrL9ZgmOTt/V3CDzi6FP4fOpLukQg9h81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTbDAX/btrL9ZgmOTt/V3CDzi6FP4fOpLukQg9h81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTbDAX%2FbtrL9ZgmOTt%2FV3CDzi6FP4fOpLukQg9h81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2274&quot; height=&quot;1300&quot; data-origin-width=&quot;2274&quot; data-origin-height=&quot;1300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&gt;잠깐 용어 정리를 하자면, &lt;b&gt;검색이 될만한 문자열을 짧게 줄여서 토큰&lt;/b&gt;이라고 부릅니다. &lt;br&gt;&lt;br&gt;&lt;b&gt;문자열을 토큰으로 만드는 과정을 토크나이징&lt;/b&gt;이라고 하고, 검색어가 &lt;b&gt;토큰으로 분리되어 저장되는 과정을 인덱싱&lt;/b&gt;이라고 부릅니다. 주목해야할 점은 토큰은 원본 데이터를 검색이 될만한 문자열로 이기 때문에 검색어 &lt;b&gt;토큰과 단순히 일치하는지만 확인&lt;/b&gt;한다는 점입니다. &lt;br&gt;&lt;br&gt;따라서 토큰을 사전순으로 정렬하여 저장한다면 이진탐색을 활용해 특정 토큰을 빠르게 찾을 수 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⭐️토크나이징 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;앞서 살펴본 것처럼 데이터를 검색하기 위해서는 검색이 될만한 &lt;b&gt;문자열로 분리하는 과정이 필수적&lt;/b&gt;입니다.&lt;br&gt;&lt;br&gt;그렇다면 &lt;b&gt;원본 데이터를 어떻게 분리&lt;/b&gt;해야 할까요? “남산전망대”의 예시처럼 여러개의 단어로 이루어진 명사의 경우 각 명사를 단위로 “남산”과 “전망대”로 쪼갤 수 있었습니다. &lt;br&gt;&lt;br&gt;같은 원리로 “성장의 즐거움을 아는 친구들, IT 개발 동아리 매시업 블로그 입니다.” 라는 문자열을 쪼개면 조사나 마침표 등 문법적인 구성 요소를 제외하고, 명사나 동사만을 분리해 “성장”, “즐거움”, “알다”, “친구”, “IT”, “개발”, “동아리”, “매시업”, “블로그” 등으로 분리할 수 있습니다.&lt;br&gt;&lt;br&gt;이처럼 문장을 구성하는 명사, 동사, 조사 등 각 단어의&lt;b&gt; &lt;/b&gt;&lt;b&gt;형태소와 품사 정보를 활용해 문자열을 분리하는 규칙&lt;/b&gt;을 정할 수 있습니다. &lt;br&gt;&lt;br&gt;하지만 단점또한 존재합니다. “아버지가방에들어셨다”와 같은 문장을 토크나이징하기 위해서는 상당히 애매모호한 부분이 존재합니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2386&quot; data-origin-height=&quot;1364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwglYy/btrL6QdT7zc/QzD7rS4NKQp9jLq60ZTgR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwglYy/btrL6QdT7zc/QzD7rS4NKQp9jLq60ZTgR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwglYy/btrL6QdT7zc/QzD7rS4NKQp9jLq60ZTgR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwglYy%2FbtrL6QdT7zc%2FQzD7rS4NKQp9jLq60ZTgR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2386&quot; height=&quot;1364&quot; data-origin-width=&quot;2386&quot; data-origin-height=&quot;1364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;“아버지가방”에서 “가”를 조사로 인식해 “아버지” “방”으로 분리할 지, 혹은 “가방”으로 인식해 “아버지” 와 “가방”으로 분리할 지 결정해야 합니다.&lt;br&gt;&lt;br&gt;규칙 기반 토크나이징의&lt;b&gt; 모호함을 보완&lt;/b&gt;하기 위해 &lt;b&gt;통계적인 방법을 활용해 토크나이징을 수행&lt;/b&gt;할 수도 있습니다. &lt;br&gt;&lt;br&gt;하나의 문장을 데이터의 연결 관계(상태 변화)로 해석하여, 학습된 데이터 연결 관계를 활용해 현재 데이터가 다음 어떤 데이터로 변화(전이)할 지 추론하는 방법입니다.&lt;br&gt;&lt;br&gt;쉽게 정리하자면 다음과 같습니다. &lt;br&gt;&lt;br&gt;일반적으로 &lt;b&gt;문장에서 첫 단어는 명사(주어)일 확률이 높고, 그 다음은 조사, 다음은 명사(목적어), 다시 조사, 마지막으로 동사(서술어)가 나올 확률이 높습니다&lt;/b&gt;.&lt;br&gt;&lt;br&gt;“아버지가방에들어가셨다”라는 예시를 다시 보겠습니다.&lt;br&gt;&lt;br&gt;“아버지”는 문장의 첫 단어이니 명사, “가”는 조사, “방”은 명사, “들어가다”는 동사로 토크나이징 하는게 확률적으로 더 올바른 토크나이징일것이라 추론할 수 있습니다.&lt;br&gt;&lt;br&gt;토크나이징은 검색 품질에 많은 영향을 주는 동시에 상당히 까다로운 부분이기도 합니다.&lt;br&gt;&lt;br&gt;언어별로 언어의 특성이 다르고 언어 활용 패턴도 달라 모든 문장에 적용할 수 있는 토크나이징 알고리즘은 찾기가 어렵습니다.&lt;br&gt;&lt;br&gt;토크나이징은 국문학과 딥러닝을 활용하여 자연어처리 분야에서 활발하게 연구되는 분야입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br&gt; 역색인의 저장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;지금까지 역색인 구조와 역색인 구조를 만들기 위한 토크나이징까지 알아봤습니다.&lt;br&gt;&lt;br&gt;토크나이징 과정을 거쳐 역색인 구조를 만들수 있었지만 &lt;b&gt;역색인 데이터를 실제로 저장&lt;/b&gt;하기 위해서는 여러 난관을 거쳐야 합니다.&lt;br&gt;&lt;br&gt;앞서 “성장의 즐거움을 아는 친구들, IT 개발 동아리 매시업 블로그 입니다.” 라는 문자열을 역색인 구조로 만들기 위해 토크나이징하면 “성장”, “즐거움”, “알다”, “친구”, “IT”, “개발”, “동아리”, “매시업”, “블로그” &lt;b&gt;총 9개의 토큰으로 분리&lt;/b&gt;됐습니다.&lt;br&gt;&lt;br&gt;즉 하나의 원본데이터를 인덱싱하기 위해서는 역색인 구조에 아홉번 쓰기를 해야합니다. 방금 예시로 든 문장은 30자 정도의 짧은 문장이었지만, 신문기사, 블로그글처럼 &lt;b&gt;아주 긴 문자열을 토크나이징 할 경우 수백개 이상의 토큰으로 분리&lt;/b&gt;될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br&gt;따라서 &lt;b&gt;역색인 구조를 저장하기 위해서는 쓰기에 강한 자료구조가 필요&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;애플리케이션에 적합한 자료구조를 설계하기 위해서는 부하에 대한 가정이 필요합니다. 부하란 “읽기 대 쓰기의 비율”, “단위 시간 당 읽기/쓰기 처리량”과 같이 &lt;b&gt;구체적이고 정량적&lt;/b&gt;으로 서술됩니다.&lt;br&gt;&lt;br&gt;일반적인 애플리케이션에서 데이터 구조는 SNS에 글을 올리는 빈도보다 SNS를 조회하는 빈도가 훨씬 많는 것 처럼 &lt;b&gt;쓰기보다 읽기 처리가 더 많을것이라는 가정&lt;/b&gt;으로 설계됐습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEFyRB/btrL9KDxqsp/LP7o0V0f3scS1ZsDbUoZGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEFyRB/btrL9KDxqsp/LP7o0V0f3scS1ZsDbUoZGK/img.png&quot; data-alt=&quot; 이미지 출처:&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;nbsp;https://benjamincongdon.me/blog/2021/08/17/B-Trees-More-Than-I-Thought-Id-Want-to-Know/ &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEFyRB/btrL9KDxqsp/LP7o0V0f3scS1ZsDbUoZGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEFyRB%2FbtrL9KDxqsp%2FLP7o0V0f3scS1ZsDbUoZGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2880&quot; height=&quot;1596&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1596&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 이미지 출처:&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;nbsp;https://benjamincongdon.me/blog/2021/08/17/B-Trees-More-Than-I-Thought-Id-Want-to-Know/ &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&gt;위 그림은 일반적으로 사용되는 색인 구조인 B+Tree입니다. 한눈에&lt;b&gt; 데이터 구조가 복잡하다&lt;/b&gt;는 것을 파악할 수 있습니다.&lt;br&gt;&lt;br&gt;데이터 구조와 구조를 변경하기 위한 규칙이 복잡하다는 것은 그만큼 찾으려는 데이터가 어디에 있고, 어떻게 접근할 수 있는지에 대해 &lt;b&gt;더 많은 정보를 담고 있다는 것을 의미&lt;/b&gt;합니다.&lt;br&gt;&lt;br&gt;따라서 읽기 시 데이터가 어떻게 저장되어 있는지에 대한 정보가 전혀 없이 탐색하는 선형탐색보다 &lt;b&gt;훨씬 효율적으로 탐색&lt;/b&gt;할 수 있습니다. 약 250테라바이트 정도의 데이터에서 원하는 데이터를 4번정도의 접근을 통해 찾을 수 있을 정도로 빠릅니다.&lt;br&gt;&lt;br&gt;하지만 &lt;b&gt;쓰기 시에는 정반대로 많은 연산&lt;/b&gt;이 필요한데요, 특히 데이터 하나의 쓰기를 위한 연산의 수는 저장되어 있는 데이터 양에 비례해 크게 증가하는 경향을 보입니다.&lt;br&gt;&lt;br&gt;예를들어 10건의 데이터가 저장되어 있을 때 새로운 데이터 하나를 저장하기 위해서는 기존 데이터를 변경하는 연산이 2번 필요했다면, &lt;br&gt;&lt;br&gt;100건의 데이터가 저장되어 있을때는 새로운 데이터 하나를 저장하기 위해 기존 데이터를 변경하는 연산이 20번 필요할 수도 있습니다.&lt;br&gt;&lt;br&gt;이를 &lt;b&gt;쓰기 증폭&lt;/b&gt;이라고 하며, 한번 쓴 데이터가 미래에도 계속 쓰기를 유발하는 현상을 의미합니다.&lt;br&gt;&lt;br&gt;정리하자면 색인에 &lt;b&gt;복잡한 데이터 구조를 사용한다면 장점으로는 읽기 질의를 효율적으로 처리할 수 있지만 &lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;단점으로는 쓰기 비용의 증가와 쓰기 증폭으로 인한 IO 대역 증가 문제가 있습니다.&lt;/b&gt;&lt;br&gt;&lt;br&gt;그렇다면 데이터 저장 구조를 아주 단순하게 구성하는 것은 어떨까요? 색인 데이터를 저장할 때 단순히 데이터를 입력된 순서대로 순차적으로 나열하기만 한다면 쓰기 비용을 크게 줄일 수 있습니다. &lt;br&gt;&lt;br&gt;하지만 데이터를 읽을 때 중복되는(덮여쓰여진)데이터가 존재하고, 무작위 순서대로 저장되어 있기 때문에 모든 데이터를 순차적으로 읽어야하는 문제가 다시 발생합니다. &lt;br&gt;&lt;br&gt;이 문제를 해결하기 위해 첫번째로 &lt;b&gt;역색인을 해시테이블 형태로 구성하는 방법을 고려&lt;/b&gt;해볼 수 있습니다.&lt;br&gt;&lt;br&gt;해시키를 토큰으로, 해시값을 토큰이 들어있는 문서 번호로 저장한다면 읽기시 빠르게 접근할 수 있고, 쓰기 연산 또한 단순해진다는 장점이 있으나&lt;br&gt;&lt;br&gt;저장되어야 하는 토큰의 수가 많아 해시테이블을 디스크에 저장해야 하고, 해시 엔트리의 주소로 사용되는 토큰의 해시값이 무작위적이기 때문에 쓰기 시에 디스크 랜덤 액세스로 인해 &lt;b&gt;성능이 크게 떨어지는 단점&lt;/b&gt;이 있습니다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br&gt;두번째로 역색인 구조를&lt;b&gt; 순차적으로 저장하는 방식과 B+트리와 같이 복잡한 데이터 구조를 사용하는 방식 사이에서 균형점&lt;/b&gt;을 찾는 방식을 고려할 수 있습니다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;핵심 아이디어는 전체 데이터를 정렬한 상태를 유지하기 보다는 &lt;b&gt;“부분적으로만 정렬된 상태를 유지하자”&lt;/b&gt;는 것입니다. 중복을 포함하여 1000개의 토큰이 순서대로 저장되는 상황을 가정해봅시다.&lt;br&gt;&lt;br&gt;먼저 순서대로 &lt;b&gt;토큰을 메모리 상에서 정렬하여 저장&lt;/b&gt;합니다.&lt;br&gt;&lt;br&gt;전체 데이터가 아닌 일부 데이터만 정렬 상태를 유지하는 것이므로 데이터 크기가 작아 메모리에 저장할 수 있고, 메모리에 저장된다면 복잡한 데이터 구조라도 디스크보다 훨씬 빠르게 정렬된 상태를 유지할 수 있습니다. 다음으로 메모리에 저장된 토큰의 수가 일정 개수 이상이 되거나, 어느정도의 시간이 지났다면 &lt;b&gt;정렬된 순서를 유지하면서 디스크에 파일로 순차적으로 저장&lt;/b&gt;합니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dVzEYx/btrMfNfbAW9/6Zi5HLe8kqEgAeKkq40kKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dVzEYx/btrMfNfbAW9/6Zi5HLe8kqEgAeKkq40kKK/img.png&quot; data-alt=&quot; Segment는 하나의 파일이다. 이미지출처:&amp;amp;amp;amp;amp;amp;amp;nbsp;https://yetanotherdevblog.com/lsm/ &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dVzEYx/btrMfNfbAW9/6Zi5HLe8kqEgAeKkq40kKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVzEYx%2FbtrMfNfbAW9%2F6Zi5HLe8kqEgAeKkq40kKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1290&quot; height=&quot;704&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; Segment는 하나의 파일이다. 이미지출처:&amp;amp;amp;amp;amp;amp;nbsp;https://yetanotherdevblog.com/lsm/ &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;일정 개수를 100개로 가정한다면 디스크에는 100개씩 데이터가 담긴 파일 10개가 생성되고, 각 파일 내에서는 데이터가 정렬된 상태를 유지하고 있습니다. 데이터 검색 요청이 들어올 때, 저장된 10개의 파일을 순차적으로 읽습니다.&lt;br&gt;&lt;br&gt;파일 내에서는 데이터가 정렬되어 있기 때문에 이진탐색을 활용하여 데이터를 효율적으로 찾을 수 있습니다. 모든 파일에서 데이터를 읽은 결과를 종합하여 중복을 제거하는 과정을 거쳐 검색 결과를 반환할 수 있습니다. &lt;br&gt;&lt;br&gt;시간이 지남에 따라 데이터가 점점 더 많아져 파일 개수가 많아지고, 중복되는 데이터가 많아진다면 읽기의 효율성 또한 점점 감소하게 됩니다. 따라서 &lt;b&gt;여러개의 파일을 하나로 합치고 덮여쓰여진 데이터를 삭제하면서 정렬해 새로운 파일을 만드는 작업을 주기적으로 실행&lt;/b&gt;해야 합니다.&lt;br&gt;&lt;br&gt;이 과정은 각 파일이 정렬되어 있기 때문에 빠르게 수행할 수 있습니다. 예를들어 한 파일에는 데이터 1, 3, 5가, 다른 파일에는 1’, 2, 4, 5가 저장되어 있다면 (1’은 데이터 1이 새로 쓰여진 데이터) 두 개의 포인터를 활용하여 덮어쓰여진 데이터를 지우고 새로운 파일로 두 파일의 데이터를 모두 정렬하여 저장하고 기존의 두 파일은 삭제하면 데이터를 효율적으로 저장할 수 있고, 새롭게 생성된 파일은 더 많은 데이터가 정렬된 상태로 저장되어 있어 읽기 성능을 유지할 수 있습니다.&lt;br&gt;&lt;br&gt;이와 같은 구조를 &lt;b&gt;LSM 트리&lt;/b&gt;(Log Structure Merge Tree)라고 합니다. LSM 트리 구조는 검색엔진 외에도 높은 쓰기 처리량을 지원하는 데이터베이스에도 흔히 사용되며, 대표적으로 HBase, Cassandra가 있습니다.&lt;br&gt;&lt;br&gt;추가적으로 데이터를 메모리에 임시적으로 저장하기 때문에 데이터의 보존을 위해 쓰기 요청 시 변경 로그를 디스크에 기록해야할 필요성이 있습니다. 변경 로그는 데이터가 디스크에 최종적으로 반영되기 전 장애 발생 시 복구를 위해 사용됩니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;지금까지 검색 기술을 지탱하는 원리에 대해 살펴봤습니다.&lt;br&gt;&lt;br&gt;검색이란 &lt;b&gt;“많은 데이터 중에서 원하는 데이터를 찾는 것”&lt;/b&gt;으로 정의할 수 있었습니다. 검색을 효율적으로 수행하기 위해 &lt;b&gt;토크나이징&lt;/b&gt;으로 역색인 구조를 구성할 수 있었습니다.&lt;br&gt;&lt;br&gt;토크나이징 방법으로는 &lt;b&gt;규칙 기반과 통계 기반 토크나이징&lt;/b&gt; 방법이 있었으며 역색인 구조를 저장하기 위해 &lt;b&gt;쓰기 처리에 강한 LSM 트리 구조를 사용&lt;/b&gt;함을 알 수 있었습니다.&lt;br&gt;&lt;br&gt;감사합니다.&lt;/p&gt;</description>
      <category>팀 이야기</category>
      <category>IT</category>
      <category>it동아리</category>
      <category>개발동아리</category>
      <category>검색</category>
      <category>동아리</category>
      <category>매쉬업</category>
      <category>매시업</category>
      <category>메쉬업</category>
      <category>역색인</category>
      <category>프로젝트</category>
      <author>판교가고싶어요</author>
      <guid isPermaLink="true">https://mash-up.tistory.com/74</guid>
      <comments>https://mash-up.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%8C%80-%EA%B2%80%EC%83%89-%EA%B8%B0%EC%88%A0%EC%9D%98-%EC%9B%90%EB%A6%AC-%EB%B0%9C%ED%91%9C-%EC%84%B8%EC%85%98#entry74comment</comments>
      <pubDate>Wed, 14 Sep 2022 22:11:43 +0900</pubDate>
    </item>
  </channel>
</rss>