<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>JAVA/CLOUD 놀이터</title>
    <link>https://golf-dev.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 3 Jul 2026 11:23:55 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>DEV_GOLF</managingEditor>
    <image>
      <title>JAVA/CLOUD 놀이터</title>
      <url>https://tistory1.daumcdn.net/tistory/4925173/attach/ead5327838734ddaacd8dc2ebeb7cccb</url>
      <link>https://golf-dev.tistory.com</link>
    </image>
    <item>
      <title>주문 부터 결제까지 사용자 상품을 잘 지켜주기 Part.2 구현</title>
      <link>https://golf-dev.tistory.com/92</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://golf-dev.tistory.com/91&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://golf-dev.tistory.com/91&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751800364706&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;주문 부터 결제까지 사용자 상품을 잘 지켜주기 Part.1 설계&quot; data-og-description=&quot;안녕하세요 ! 오랜만에 새로운 글로 찾아온 GOLF 입니다! 내용 요약GPT 발췌1. 문제 상황사용자가 제일 먼저 구매했음에도 결제까지 완료되지 않으면, 다른 사용자가 결제를 진행하여 상품을 선점&quot; data-og-host=&quot;golf-dev.tistory.com&quot; data-og-source-url=&quot;https://golf-dev.tistory.com/91&quot; data-og-url=&quot;https://golf-dev.tistory.com/91&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RNEvJ/hyZf1TgK8R/NgneGHR6NFzlIkBE0Qv8Tk/img.png?width=800&amp;amp;height=435&amp;amp;face=0_0_800_435,https://scrap.kakaocdn.net/dn/nViUu/hyZfWRZWUU/nvX5b3xPPKEsph76fddPV0/img.png?width=800&amp;amp;height=435&amp;amp;face=0_0_800_435,https://scrap.kakaocdn.net/dn/cC8GmH/hyZf94Ortc/kuvx9NGFikMm8MoC2X1qek/img.png?width=4979&amp;amp;height=2897&amp;amp;face=0_0_4979_2897&quot;&gt;&lt;a href=&quot;https://golf-dev.tistory.com/91&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://golf-dev.tistory.com/91&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RNEvJ/hyZf1TgK8R/NgneGHR6NFzlIkBE0Qv8Tk/img.png?width=800&amp;amp;height=435&amp;amp;face=0_0_800_435,https://scrap.kakaocdn.net/dn/nViUu/hyZfWRZWUU/nvX5b3xPPKEsph76fddPV0/img.png?width=800&amp;amp;height=435&amp;amp;face=0_0_800_435,https://scrap.kakaocdn.net/dn/cC8GmH/hyZf94Ortc/kuvx9NGFikMm8MoC2X1qek/img.png?width=4979&amp;amp;height=2897&amp;amp;face=0_0_4979_2897');&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;주문 부터 결제까지 사용자 상품을 잘 지켜주기 Part.1 설계&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요 ! 오랜만에 새로운 글로 찾아온 GOLF 입니다! 내용 요약GPT 발췌1. 문제 상황사용자가 제일 먼저 구매했음에도 결제까지 완료되지 않으면, 다른 사용자가 결제를 진행하여 상품을 선점&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;golf-dev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 위 글에서 문제를 해결하기 위한 선점 매커니즘을 설계했습니다. 이제 실제로 구현을 해보겠습니다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현 하기전에&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Event란?&lt;br /&gt;&lt;br /&gt;주문 같은 Aggregate는 자기 도메인 로직에만 집중하고, 주문 완료 후 재고 선점과 같은 처리는 Domain Event로 발행합니다.&lt;br /&gt;Spring이 제공하는 ApplicationEventPublisher 와 @EventListener 덕분에 이런 Domain Event를 간단히 발행하고 구독 할 수 있습니다!&lt;br /&gt;덕분에 포인트 적립, 알림 발송, 회계 처리처럼 다른 Aggregate나 외부 시스템과 연결된 작업들을 핵심 트랜잭션과 분리해서 안전하게 처리 할 수 있습니다.&lt;br /&gt;&lt;br /&gt;결국, Spring Event는 Domain Event 패턴을 서비스에 자연스럽게 녹여낼 수 있게 도와주는 실질적인 도구 라고 보면 됩니다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주문 API 구현&lt;/h3&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Transactional
override fun order(ticketIds: List&amp;lt;Long&amp;gt;, userId: Long): CreateOrderResponseMessage {
    val tickets: List&amp;lt;Ticket&amp;gt; = ticketRepository.findAllByOrderId(ticketIds)

    validateItemStatus(tickets, ticketIds)

    val orderId = orderIdGenerator.generateOrderId()
    val order = Order.order(orderId, userId, tickets)

    // 선점 이벤트 시작
    eventPublisher.publishEvent(OrderCompleteEvent(orderId, ticketIds))

    val savedOrder: Order = orderRepository.save(order)
    return CreateOrderResponseMessage.from(savedOrder)
}

private fun validateItemStatus(tickets: List&amp;lt;Ticket&amp;gt;, ticketIds: List&amp;lt;Long&amp;gt;) {
    if (stockRepository.alreadyReserveByTicketIds(ticketIds)) {
        throw IllegalArgumentException(&quot;이미 선점중인 상품입니다.&quot;)
    }
    
    // 기타 검증 로직 ... //
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 구매한 상품 중 이미 선점 중인 상품이 있는지 Redis에서 조회합니다. 만약 이미 선점되어 있다면 예외를 던집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 선점중인 상품이 없다면 주문 정보를 생성합니다. 이후 주문 완료가 되면 응답 객체를 만들어 반환하고 선점 이벤트를 호출합니다.&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;여기서 핵심은 선점 이벤트를 호출하여 주문 Transaction과 분리하는 건데요. 이유는 간단합니다. 재고 선점을 실패했을 때 주문도 실패한다면 Redis 장애가 주문 전체에 대한 장애로 이어질 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는, 사용자에게 큰 불편을 느끼게 해줍니다. 재고 선점이 주문 로직에 영향을 주지 않게 하기 위해 Spring Event를 이용해서 주문 로직이 성공한 이후 동작할 수 있도록 수정하였습니다. 그렇다면 이제 Event Listener 로직을 살펴보겠습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handleOrderCompleteEvent(event: OrderCompleteEvent) {
    log.info(&quot;선점 정보 저장 시작 : 주문 ID : {}&quot;, event.orderId)
    val result = kotlin.runCatching { stockRepository.reserveStock(orderId = event.orderId, itemIds = event.ticketIds) }

    result.onFailure {
        log.error(&quot;주문 ID : {}, 선점 실패 사유 : {}&quot;, event.orderId, it.message)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발행 된 이벤트를 받아 주문을 선점합니다. 이 때 비동기로 동작하기 때문에 이슈 트래킹을 위해 orderId 를 기준으로 로그를 남깁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결제 API 구현&lt;/h3&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Transactional
override fun checkout(message: CheckoutRequestMessage): CheckoutCompleteResponseMessage {
    val order = orderRepository.findByIdAndUserId(message.orderId, message.userId)
    val tickets = ticketRepository.findAllByOrderId(order.orderItem.map { it.itemId })

    kotlin.runCatching { validateTickets(tickets, message) }
        .onFailure { publishFailureEventAndThrow(message, it) }

    eventPublisher.publishEvent(CheckoutSuccessEvent(order.orderId, tickets.map { it.id }))
    
    val payment = order.payment ?: createAndSaveNewPayment(order, message.paymentMethod)
    
    return CheckoutCompleteResponseMessage(order.orderId, order.amount, payment.idempotentKey)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 고객이 주문한 상품외에 다른 고객이 선점한 정보가 있다면 예외를 던집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 자신의 주문이 선점 중이라면선점 시간을 갱신합니다. 선점 중이아니라면 다시 한 번 선점 시도를 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 이후 결제를 완료 시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때도 마찬가지로 선점 관련 로직은 모두 Spring Event로 checkout 트랜잭션과는 따로 로직이 돌 수 있도록 하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handleStockReservation(event: CheckoutSuccessEvent) {
    log.info(&quot;선점 정보 갱신 시작 : 주문 ID : {}&quot;, event.orderId)

    kotlin.runCatching { updateStockReservationProcess(event) }
        .onFailure { log.info(&quot;선점 실패 : {}&quot;, event.orderId) }
}

private fun updateStockReservationProcess(event: CheckoutSuccessEvent) {
    if (stockRepository.existsReserveByOrderId(event.orderId)) {
        stockRepository.updateTtl(event.orderId)
        return
    }

    retryReserveStock(event.orderId, event.ticketIds)
}

private fun retryReserveStock(orderId: String, ticketIds: List&amp;lt;Long&amp;gt;) {
    val reserveResult = stockRepository.reserveStock(orderId, ticketIds)

    if (!reserveResult) {
        throw IllegalArgumentException(&quot;상품 선점에 실패했습니다.&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 정보 정산에 성공하면 결제 전까지 고객이 결제를 진행할 수 있도록 선점 정보를 갱신해주고 만약 주문에서 모종의 이유로 선점을 하지 못한 경우에 대비해 한 번 더 선점 시도 할 수 있도록 합니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
fun handleOrderFailEvent(event: OrderFailEvent) {
    log.info(&quot;결제/checkout 실패 후 선점 롤백 시작 : {}&quot;, event.orderId)
    val result = kotlin.runCatching { stockRepository.cancelReserve(event.orderId) }

    result.onFailure {
        log.info(&quot;주문 ID : {} 선점 실패 사유: {}&quot;, event.orderId, it.message)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 결제 정산에 실패하면 롤백을 하기 위해 재고 선점을 해제해줍니다. (장시간 선점되어 다른 고객들이 좌석이 있음에도 살 수 없는 현상을 방지하기 위함입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 결제 시엔 성공 또는 실패 시 모두 선점을 해제하여 고객들이 좌석을 원할하게 구매할 수 있도록 재고 선점을 지속적으로 관리해주고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 부터 결제까지 재고를 지킬 수 있도록 선점 로직을 어떻게 구현하는지에 대해서 살펴봤습니다. 독자 여러분께 많은 도움이 되었으면 좋겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피드백은 대환영입니다!&lt;/p&gt;</description>
      <category>개발론(아키텍처 설계, 객체 지향 등)</category>
      <author>DEV_GOLF</author>
      <guid isPermaLink="true">https://golf-dev.tistory.com/92</guid>
      <comments>https://golf-dev.tistory.com/92#entry92comment</comments>
      <pubDate>Sun, 6 Jul 2025 22:31:59 +0900</pubDate>
    </item>
    <item>
      <title>주문 부터 결제까지 사용자 상품을 잘 지켜주기 Part.1 설계</title>
      <link>https://golf-dev.tistory.com/91</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요 ! 오랜만에 새로운 글로 찾아온 GOLF 입니다!&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내용 요약&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;GPT 발췌&lt;br /&gt;&lt;br /&gt;1. 문제 상황&lt;br /&gt;사용자가 제일 먼저 구매했음에도 결제까지 완료되지 않으면, 다른 사용자가 결제를 진행하여 상품을 선점할 수 있는 문제가 발생합니다. &lt;br /&gt;&lt;br /&gt;2. 해결 방법 비교:&lt;br /&gt;주문 즉시 판매 처리 (보상 트랜잭션 방식): 주문 정보를 즉시 &amp;ldquo;팔림&amp;rdquo; 상태로 처리하여 다른 사용자가 결제하지 못하게 합니다. &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;Redis vs. RDBMS(MySQL):&lt;br /&gt;Redis: 빠른 in-memory 처리로 대규모 트래픽 환경에서 높은 처리량과 낮은 구현 난이도를 제공합니다. 단, 데이터 영속성은 보완적으로 고려해야 합니다. &lt;br /&gt;&lt;br /&gt;MySQL: 동기 I/O 방식으로 데이터 안정성과 영속성이 뛰어나지만, 동시성 처리와 대규모 트래픽 처리에서는 성능 면에서 Redis에 비해 불리합니다.&lt;br /&gt;&lt;br /&gt;따라서, 선점 정보는 Redis를 활용해 빠르게 처리하고, 주문 및 결제 정보는 MySQL과 같이 안정적인 저장소에 기록하는 하이브리드 아키텍처를 제안합니다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛날에 티켓을 사다보면 항상 걱정했던 부분이 좌석 선택부터 결제까지 너무 오래 걸려서 남들이 먼저 결제해버리면 어떡하지? 했던 경험이 있는데요 필자인 저 또한 그래서 항상 무통장 입금을 전제로 빠르게 결제를 마무리했던 경험이 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;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;h3 data-ke-size=&quot;size23&quot;&gt;어떻게 해결해볼 수 있을까 (접근법)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 상황은 간단합니다! 제일 먼저 구매했음에도 결제 까지 가지 않으면 다른 사람도 결제 할 수 있다. 가 문제 상황입니다. 그렇다면?!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문을 했을 때 아예 팔렸다로 처리하여 다른 사람이 결제 하지 못하게 막는다.&lt;/li&gt;
&lt;li&gt;주문이 들어오면 해당 티켓은 결제 전 까지 잠궈둔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문이 팔렸다로 처리하는 방법을 먼저 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5144&quot; data-origin-height=&quot;2800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctTBHe/btsMS6FVUx4/BNhunOpeEfTeTVhWaKdyQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctTBHe/btsMS6FVUx4/BNhunOpeEfTeTVhWaKdyQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctTBHe/btsMS6FVUx4/BNhunOpeEfTeTVhWaKdyQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctTBHe%2FbtsMS6FVUx4%2FBNhunOpeEfTeTVhWaKdyQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;5144&quot; height=&quot;2800&quot; data-origin-width=&quot;5144&quot; data-origin-height=&quot;2800&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 주문 비즈니스가 복잡해지면 그 만큼 주문 롤백을 위해 처리하는 비용은 증가할 것입니다. 보상 트랜잭션 실패 시 처리 과정 또한 복잡해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 잠금 매커니즘을 사용하면 어떨까요?&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5144&quot; data-origin-height=&quot;2800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTF4C8/btsMS60dMFk/wEIx3hIp1zvAPnlK2mGuDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTF4C8/btsMS60dMFk/wEIx3hIp1zvAPnlK2mGuDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTF4C8/btsMS60dMFk/wEIx3hIp1zvAPnlK2mGuDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTF4C8%2FbtsMS60dMFk%2FwEIx3hIp1zvAPnlK2mGuDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;5144&quot; height=&quot;2800&quot; data-origin-width=&quot;5144&quot; data-origin-height=&quot;2800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플로우는 변하지 않습니다. 다만 주문 완료 -&amp;gt; 선점, 주문 처리 롤백 -&amp;gt; 선점 해제 로 바뀌었을 뿐입니다. 선점은 비교적 복잡한 주문 롤백 문제를 깔끔하게 해결해줍니다. 선점은 해당 티켓을 잠궈만 놓고 실제로 주문 처리 하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 처리를 하지 않았기 때문에 결제나 주문 중 문제가 생기더라도 잠금을 해제만 시켜주면 바로 다른 사용자가 살 수 있습니다. 그리고 비즈니스의 영향도 받지 않기 때문에 주문 비즈니스가 복잡해지더라도 선점 해제 로직이 복잡해질 확률은 비교적 낮습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 단점은 잠금 해제와 획득을 위한 추가적인 작업 비용이 들어간다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표로 정리해보면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 80px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 18.1007%; height: 20px;&quot;&gt;비교 항목&lt;/td&gt;
&lt;td style=&quot;width: 37.1706%; height: 20px;&quot;&gt;상품 판매 처리 방식&lt;/td&gt;
&lt;td style=&quot;width: 44.7286%; height: 20px;&quot;&gt;상품 잠금 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 18.1007%; height: 20px;&quot;&gt;구현 난이도&lt;/td&gt;
&lt;td style=&quot;width: 37.1706%; height: 20px;&quot;&gt;어려움 로직이 복잡해질 수 있음.&lt;/td&gt;
&lt;td style=&quot;width: 44.7286%; height: 20px;&quot;&gt;동시성 제어도 명확하고 구현 자체도 단순&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 18.1007%; height: 20px;&quot;&gt;예외 처리&lt;/td&gt;
&lt;td style=&quot;width: 37.1706%; height: 20px;&quot;&gt;복잡한 트랜잭션 흐름과 상태 관리로 인한 &lt;br /&gt;예외처리 복잡도 높음&lt;/td&gt;
&lt;td style=&quot;width: 44.7286%; height: 20px;&quot;&gt;단일 흐름으로 처리되어 예외 처리 시스템 구축이 단순함.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 18.1007%; height: 20px;&quot;&gt;시스템 복잡도&lt;/td&gt;
&lt;td style=&quot;width: 37.1706%; height: 20px;&quot;&gt;복잡도높음&lt;/td&gt;
&lt;td style=&quot;width: 44.7286%; height: 20px;&quot;&gt;복잡도 낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 잠금 방식이 구현 난이도도 훨씬 간결하고 어렵지 않게 문제를 해결할 수 있기 때문에 잠금 방식으로 문제를 해결하는 것이 가장 좋아보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시스템 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 시스템을 설계 해볼 건데요. 잠금은 어떻게 설계해보면 좋을까요?&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레디스를 이용한 방법&lt;/li&gt;
&lt;li&gt;RDBMS를 이용한 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS는 간결하고 영속화가 된다는 점에 장점을 갖고 있는 저장소입니다. 반면에 Redis 는 신속한 처리속도와 높은 수준의 처리량을 갖는 저장소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 두 인프라 모두 각자의 방식으로 동시성을 제어하고 있죠. 우리가 여기서 따져봐야할 것은 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현이 간결한가?&lt;/li&gt;
&lt;li&gt;동시성 문제로 인한 추가 Lock 매커니즘 구현 복잡도가 있는가?&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;대규모 트래픽을 고려했을 때 순간 요청이 몰리는 순간 높은 처리량으로 병목을 방지할 수 있는가?&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 관점으로 봤을 때 표로 나타내면 두 인프라간 차이는 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.5659%;&quot;&gt;비교항목&lt;/td&gt;
&lt;td style=&quot;width: 31.4728%;&quot;&gt;Redis&lt;/td&gt;
&lt;td style=&quot;width: 49.9612%;&quot;&gt;RDBMS (MySQL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.5659%;&quot;&gt;구현 난이도&lt;/td&gt;
&lt;td style=&quot;width: 31.4728%;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;width: 49.9612%;&quot;&gt;높음 &lt;br /&gt;(TTL 같은 게 없기 때문에 lock release를 고려해야함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.5659%;&quot;&gt;동시성 문제&lt;/td&gt;
&lt;td style=&quot;width: 31.4728%;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;width: 49.9612%;&quot;&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.5659%;&quot;&gt;대규모 트래픽 안정성&lt;/td&gt;
&lt;td style=&quot;width: 31.4728%;&quot;&gt;높음&lt;br /&gt;(in-memory &amp;amp; multi-plex I/O)&lt;/td&gt;
&lt;td style=&quot;width: 49.9612%;&quot;&gt;비교적 낮음 &lt;br /&gt;(대부분 요청에서 동기 방식 I/O 방식 사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.5659%;&quot;&gt;데이터 안정성&lt;/td&gt;
&lt;td style=&quot;width: 31.4728%;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;width: 49.9612%;&quot;&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Deep 한 부분은 연관성이 떨어져 이 chapter 에선 설명하지 않습니다.&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;선점은 선점 정보가 영속화 될 필요 없이 TTL 에 따라서 바로 바로 날려주어야합니다. 또한 중요한 데이터라고 보기도 어렵습니다. 그리고 무엇보다 몰리는 대규모 트래픽 환경에선 처리량이 높은 Redis가 더 적절하다고 판단됩니다.&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;결론적으로 Redis를 이용하여 처리하기로 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 아키텍처를 보면 다음 과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4979&quot; data-origin-height=&quot;2897&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wfywT/btsMTfiz3Zx/6pIqfe3ZR2Dw1tk7VeXRxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wfywT/btsMTfiz3Zx/6pIqfe3ZR2Dw1tk7VeXRxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wfywT/btsMTfiz3Zx/6pIqfe3ZR2Dw1tk7VeXRxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwfywT%2FbtsMTfiz3Zx%2F6pIqfe3ZR2Dw1tk7VeXRxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4979&quot; height=&quot;2897&quot; data-origin-width=&quot;4979&quot; data-origin-height=&quot;2897&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;1. 주문 및 결제 시 Redis에 선점 정보를 저장하고 상품이 안전하게 처리되도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 결제와 주문 정보는 안전한 RDBMS에 저장하면서 주문과 결제 상태를 영속화 하여 지속적으로 트래킹 가능하게 처리해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 선점에는 반드시 TTL을 포함하여 어뷰징으로 부터 보호합니다. 적절한 TTL을 걸어주어 의도적인 어뷰징을 막습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 결제 완료가 되거나 결제 중간에 오류로 인해서 취소된다면 선점을 해제해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 까지가 문제 해결을 위한 접근 및 시스템 아키텍처 설계 얘기였습니다. part.2 에서는 선점 로직을 직접 구현하면서 구현전략에 대한 자세한 얘기를 진행해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침.&lt;/p&gt;</description>
      <category>개발론(아키텍처 설계, 객체 지향 등)</category>
      <category>E-Commerce</category>
      <category>lock</category>
      <category>MySQL</category>
      <category>Redis</category>
      <category>Spring</category>
      <category>ttl</category>
      <category>개발</category>
      <category>선점</category>
      <category>시스템아키텍처</category>
      <author>DEV_GOLF</author>
      <guid isPermaLink="true">https://golf-dev.tistory.com/91</guid>
      <comments>https://golf-dev.tistory.com/91#entry91comment</comments>
      <pubDate>Sat, 22 Mar 2025 16:07:08 +0900</pubDate>
    </item>
    <item>
      <title>실무에서 안전한 코드 작성을 위한 방패: Defensive Copy와 불변성</title>
      <link>https://golf-dev.tistory.com/90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 개발 과정에서 예기치 못한 상태 변경으로 인한 버그를 마주합니다. 이런 경우 디버깅도 힘들어지고 이후 객체의 개발 의도가 변경되고 비대해지면서 더 찾기가 힘들어집니다.&amp;nbsp;이런 문제를 개선하기 위해선 어떤 방법이 있을까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;불변성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불변성이란 한번 생성된 객체는 이후 어떤 방법으로든 상태가 변경되지 않는 것을 말합니다. 불변 객체는 상태가 바뀌지 않기 때문엔 여러 스레드에서 동시접근 해도 안전하고, 예측가능하고 디버깅이 쉬운 코드 작성을 돕습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 final class로 불변 클래스를 선언해주고 내부적으로도 상태를 변경하는 코드가 없어야 합니다. 다음은 불변을 위반한 상황입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723137749513&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val person = Person(&quot;1996-08-12&quot;, &quot;golf&quot;, 27, 179, 72)

println(person.toString()) // 나이 : 27

person.age++

println(person.toString()) // 나이 : 28&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;val person은 불변하는 변수이지만 내부 상태가 변경 가능하기 때문에 변경이 외부 어디서든 자유롭게 변경이 가능하며 여러 스레드가 상태를 동시에 변경도 가능합니다. 가급적 피해야하는 구현입니다.&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;이러한 솔루션을 위해 불변 객체를 선언해야하는데 대표적으로 Kotlin에 List가 있습니다.&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;520&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1sLgY/btsIYw9IY9K/kgrlfk1MTkayCt51lPEIO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1sLgY/btsIYw9IY9K/kgrlfk1MTkayCt51lPEIO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1sLgY/btsIYw9IY9K/kgrlfk1MTkayCt51lPEIO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1sLgY%2FbtsIYw9IY9K%2Fkgrlfk1MTkayCt51lPEIO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;520&quot; height=&quot;206&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 코틀린에선 List 내부의 상태를 변경할 수 없습니다. 철저한 불변이라고 볼 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 경우 멀티스레드의 경쟁조건과 외부 무분별한 변경으로 부터 우리 비즈니스를 보호할 수 있습니다. 변경되지 않는다는 강력한 전제로 인해 코드도 간결해집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이러한 불변 객체가 상태를 변경해야하는 요구사항이 생긴다면 어떻게 해야할까요? &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;불변 객체는 아까도 말씀드렸다 싶이 상태를 변경하지 못합니다. 그렇다고 해도 상태를 변경해야하는 경우 불변 객체를 깨버리는 경우 사이드 이펙트는 커질 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Defensive Copy&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defensive copy란 방어적 복사를 뜻합니다. defensive copy를 통해서 위 문제를 해결할 수 있다. 방법은 간단하다. 내부 상태를 똑같이 들고 있는 복사본을 새로운 객체에 담아 리턴하는 방식이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723138425568&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person(
    private val birth,
    private val name,
    private val age,
    private val height,
    private val weight
) {

    fun copy(): Person {
    	new Person(
            birth = this.birth,
            name = this.name,
            age = this.age,
            height = this.height,
            weight = this.weight
        )
    }
}&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;Person 클래스는 copy할 때 본인 클래스를 그대로 리턴하는것이 아닌 다른 인스턴스지만 똑같은 정보를 갖는 인스턴스를 반환합니다. 이럴 경우 디버깅 시 외부에서 변경될 이유가 없기 때문에 copy시 이전과 현재 상태만 확인해본다면 쉽게 오류 원인을 찾을 수 있을 것입니다.&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;typescript 에서도 마찬가지입니다. 대표적으로 mutableList와 deep copy를 구현한 예제를 찾아봅시다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1c2128; color: #c9d1d9;&quot;&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import _ from &quot;lodash&quot;

class MutableList&amp;lt;T&amp;gt; {
    
    private list: T[] = [];
    
    constructor(list: T[]) {
        this.list = list;
    }
    
    public add(item: T): MutableList&amp;lt;T&amp;gt; {
        const copyList = _.cloneDeep(this.list);
        copyList.push(item);
        return new MutableList(copyList);
    }
    
    public remove(index: number): MutableList&amp;lt;T&amp;gt; {
        const copyList = _.cloneDeep(this.list);
        copyList.splice(index, 1);
        return new MutableList(copyList);
    }
    
    // ... 이후 구현
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제는 lodash 라이브러리를 사용하여 list deep copy 후 필요한 연산 후 새로운 인스턴스를 반환해주고 있습니다. 마찬가지로 상태가 변경 되는게 아닌 상태가 다른 새로운 객체가 생기기 때문에 안정적이고 간결한 코딩이 가능해집니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Deep Copy?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 객체의 모든 필드와 그 필드들이 참조하는 객체들까지 모두 복사하여 새로운 객체를 생성하는 것을 말합니다. 즉, 원본 객체와 복사된 객체는 완전히 독립적인 상태가 되며, 하나의 객체를 수정해도 다른 객체에 영향을 미치지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, 얕은 복사(Shallow Copy)는 최상위 객체의 필드만 복사하며, 객체가 참조하는 하위 객체들은 참조만 복사합니다. 따라서 원본 객체와 복사된 객체는 하위 객체를 공유하게 되어, 하위 객체의 변경은 두 객체 모두에 영향을 미칠 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제 같은 경우 typescript에서 Deep copy를 사용했는데 이유는 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태 관리 : 프론트엔드 애플리케이션에서는 상태(state)를 관리하는 것이 중요합니다. 상태가 변경될 때마다 UI를 업데이트해야 하기 때문에, 상태 변경이 발생할 때 불변성을 유지하는 것이 중요합니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;참조 문제 방식: 얕은 복사는 객체의 참조를 복사하기 때문에 원본 객체와 복사된 객체가 동일한 하위 객체를 참조하게 됩니다. 이는 원치 않는 부작용(side effects)을 발생시킬 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typescript가 많이 쓰이는 FE 진영에선 다음과 같은 이유 등으로 deep copy&amp;nbsp; 및 defensive copy가 적극적으로 쓰이고 있습니다.&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;그럼 단순 장점만 있을까요? 불변 객체 및 defensive copy는 다음을 주의하여야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체가 너무 많이 사용하여 memory 사용량이 증가 한다. 대량의 객체 생성이 readonly임에도 불구하고 defensive copy를 사용한다면 오히려 득보단 실이 많을 것이다. 이럴떈 불변 객체로 선언한 후 defensive copy를 미적용하는 것도 좋습니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;개발 생산성이 느려질 수도 있다. defensive copy를 적용하기 위해 immer, lodash 등 적절한 라이브러리를 선택해야할 것이고 defensive copy에 대한 이해와 구현 노력 비용이 들어간다. 한시가 급한 비즈니스면 패스하는 것도 좋은 선택입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defensive copy와 deep copy 불변성에 대해서 알아보았습니다. FE 진영에서 정말 중요하고 특히 함수형 프로그래밍의 핵심 가치중 하나인 순수함수를 만들기 위해선 상태가 매우 중요하게 다뤄집니다. 외부의 영향을 안받는 순수함수를 만들고 또 외부에 영향 받지 않는 불변 객체를 만들어 복잡한 체이닝 함수에서 상태가 일관될 수 있도록 하고 변경 추적이 용이하게 만들기 위해 필수입니다.&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;꼭 FE 개발자라면 공부해보시길 바랍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침.&lt;/p&gt;</description>
      <category>개발론(아키텍처 설계, 객체 지향 등)</category>
      <author>DEV_GOLF</author>
      <guid isPermaLink="true">https://golf-dev.tistory.com/90</guid>
      <comments>https://golf-dev.tistory.com/90#entry90comment</comments>
      <pubDate>Fri, 9 Aug 2024 02:53:02 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot + JPA] 게시판  프로젝트 JWT 구현</title>
      <link>https://golf-dev.tistory.com/89</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;세팅글을 쓴다는게 너무 오랫동안 방치되다보니 과거 코드보단 그래도 나름 최신 코드가 예제로 쓰기 좋은거 같아 코틀린 최신 코드로 되어있다는 점 양해부탁드립니다.&lt;/p&gt;
&lt;h1&gt;여기서 잠깐&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 읽기 전 사전 지식이 필요합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;전자 서명: 전자 서명은 디지털 형식으로 문서나 데이터의 서명 및 인증을 수행하는 방법입니다. 전자 서명은 종이 서명의 디지털 대응물로, 문서의 진위성, 무결성, 서명자의 신원을 확인하는 데 사용됩니다.&lt;br /&gt;&lt;br /&gt;주요 특징은 다음과 같습니다.&lt;br /&gt;&lt;br /&gt;1. 인증: 서명자가 누구인지 확인할 수 있습니다.&lt;br /&gt;2. 무결성: 서명된 문서가 변경되지 않았음을 보장합니다.&lt;br /&gt;&lt;br /&gt;디지털 서명의 동작 원리&lt;br /&gt;디지털 서명은 공공 키 기반 구조(PKI, Public Key Infrastructure)를 사용합니다.&lt;br /&gt;키 쌍 생성: 서명자는 공개 키와 비밀 키 쌍을 생성합니다.해시 생성: 서명할 데이터에 대해 해시 함수(예: SHA-256)를 사용하여 고정 길이의 해시 값을 생성합니다.&lt;br /&gt;&lt;br /&gt;서명 생성: 비밀 키로 해시 값을 암호화하여 디지털 서명을 생성합니다.서명 검증: 수신자는 서명자의 공개 키를 사용하여 서명을 복호화하고, 데이터에 대해 동일한 해시 함수를 적용하여 얻은 해시 값과 비교하여 서명을 검증합니다.&lt;br /&gt;&lt;br /&gt;이걸 이해하셔야지만 전자서명으로인해 출처를 믿고 사용할 수 있다라는 것을 이해할 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 JWT구현에 들어가기전 JWT가 무엇인지 알아볼까요?&lt;/p&gt;
&lt;h1&gt;JWT란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 Json Web Token의 앞 철자만 따온 단어로 Server와 Client간 출처를 명확하게 하여 서로 신뢰를 갖고 사용하기 위해 사용되는 web token입니다. 주로 인증이나 인가에 많이 사용되며 정보가 적은 경우 저장소 없이 token에만 저장하여 stateless한 protocol 환경에서 유리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stateless한 protocol에서 유리한 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버와 저장소로부터 인증 정보를 지속적으로 받아와야하는 session과 달리 jwt 그 자체에 인증정보를 담고 있기 때문에 저장소 없이 jwt 내부 정보 열람이 가능합니다.&lt;/li&gt;
&lt;li&gt;전자서명하기위한 인증키를 서버에 저장하고 외부로 노출 없이 관리한다면 외부에서 내부 정보 열람이나 토큰 변경 없었다는것을 보증할 수 있어 신뢰를 갖고 토큰 내부 정보에 있는 데이터로 인증 인가가 가능합니다.&lt;/li&gt;
&lt;li&gt;저장소 없이 인증 인가가 가능하기 때문에 scale-out 시 database나 redis 같은 저장소 고민을 할 필요가 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT가 다음과 같이 안전할 수 있는 이유는 뭘까요? 구조를 보면 힌트를 얻을 수 있는데 구조는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;577&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SR07X/btsHA2pbSEH/bRI3vNrK3kjhFdkEYXvqB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SR07X/btsHA2pbSEH/bRI3vNrK3kjhFdkEYXvqB1/img.png&quot; data-alt=&quot;출처 :&amp;amp;nbsp;https://velog.io/@fill0006/JWTJSON-Web-Token-%EA%B5%AC%EC%A1%B0-%EB%B0%8F-%EC%9D%B4%ED%95%B4&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SR07X/btsHA2pbSEH/bRI3vNrK3kjhFdkEYXvqB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSR07X%2FbtsHA2pbSEH%2FbRI3vNrK3kjhFdkEYXvqB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1057&quot; height=&quot;577&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;577&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://velog.io/@fill0006/JWTJSON-Web-Token-%EA%B5%AC%EC%A1%B0-%EB%B0%8F-%EC%9D%B4%ED%95%B4&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Header: 해싱할 때 사용한 알고리즘 정보와 토큰의 타입 정보를 포함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Payload: claim들이 들어있습니다. 주로 인증된 사용자 정보나 인가 정보등을 갖고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Signature: 헤더와 페이로드를 인코딩한 후, 비밀 키를 사용하여 해상한 값입니다. 이는 토큰의 무결성과 출처를 확인하는 데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조의 구분은 .으로 이루어져 위 그림같은 형태의 JWT가 생성됩니다.&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;JWT의 인증 순서는 다음과 같습니다.&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;1765&quot; data-origin-height=&quot;611&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lRpb6/btsHBz1bfN4/uYPB0mnKP8UuBZwXF9dpQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lRpb6/btsHBz1bfN4/uYPB0mnKP8UuBZwXF9dpQk/img.png&quot; data-alt=&quot;요청 흐름도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lRpb6/btsHBz1bfN4/uYPB0mnKP8UuBZwXF9dpQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlRpb6%2FbtsHBz1bfN4%2FuYPB0mnKP8UuBZwXF9dpQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1004&quot; height=&quot;348&quot; data-origin-width=&quot;1765&quot; data-origin-height=&quot;611&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;요청 흐름도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Client가 JWT 정보를 Request Header에 Authorization Header에 담아 Bearer Token 형태로 WAS에 인증 요청을 보냅니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Server는 JWT Filter에서 받아 1차적으로 인증을 시도합니다. 이 때 Authorization Header로부터 Bearer Token을 받아옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Bearer를 떼어낸 JWT 토큰만을 받아와 전자 서명정보와 유효한 토큰인지 확인합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-1. 실패할 경우엔 401 UnAuthorization Error를 Client에 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 성공한 경우엔 Token안에 Claims를 열람하여 회원 정보를 가져옵니다. 열람한 정보를 갖고 이후 요청을 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 코드를 보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1716561255442&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class JwtFilter(
    private val tokenProvider: TokenProvider,
    private val log: Logger
) : GenericFilterBean() {

    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        val httpServletRequest = request as HttpServletRequest
        val jwt = resolveToken(httpServletRequest)?: &quot;&quot;
        val requestURI = httpServletRequest.requestURI

        if (StringUtils.hasText(jwt) &amp;amp;&amp;amp; tokenProvider.validateToken(jwt)) {
            val authentication = tokenProvider.getAuthentication(jwt)
            SecurityContextHolder.getContext().authentication = authentication
            log.debug(&quot;Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}&quot;, authentication.name, requestURI)
        } else {
            log.debug(&quot;유효한 JWT 토큰이 없습니다, uri: {}&quot;, requestURI)
        }

        chain.doFilter(request, response)
    }

    private fun resolveToken(request: HttpServletRequest): String? {
        val bearerToken = request.getHeader(AUTHORIZATION_HEADER)

        return if (StringUtils.hasText(bearerToken) &amp;amp;&amp;amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
            bearerToken.substring(7)
        } else null
    }

    companion object {
        const val AUTHORIZATION_HEADER = &quot;Authorization&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 HttpServletRequest로부터 jwt를 받아오고 이후 검증을 통해 authentication 정보를 받아오고 있습니다. 그리고 인증된 정보를 Security Context에 넣어준 후 다음 filter로 넘깁니다.&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;그렇다면 검증과 authentication 정보는 어떻게 받아올까요?&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1716561457079&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class TokenProvider(
    @param:Value(&quot;\${jwt.secret}&quot;) private val secret: String,
    @Value(&quot;\${jwt.accessToken-validity-in-seconds}&quot;) private val accessTokenValidityInSeconds: Long,
    @Value(&quot;\${jwt.refreshToken-validity-in-seconds}&quot;) private val refreshTokenValidityInSeconds: Long,
    private val customUserDetailsService: CustomUserDetailsService,
    private val log: Logger
) : InitializingBean {

    private val accessTokenValidityInMilliSeconds: Long = accessTokenValidityInSeconds * 1000
    private val refreshTokenValidityInMilliSeconds: Long = refreshTokenValidityInSeconds * 1000
    private var key: Key? = null

    override fun afterPropertiesSet() {
        key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret))
    }

    fun createToken(memberId: Long, authentication: Authentication): TokenBaseDto {
        val authorities = authentication.authorities.stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.joining(&quot;,&quot;))

        val accessTokenExpiredTime = Date(Date().time + accessTokenValidityInMilliSeconds)
        val refreshTokenExpiredTime = Date(Date().time + refreshTokenValidityInMilliSeconds)

        val accessToken = Jwts.builder()
            .claim(&quot;memberId&quot;, memberId.toString())
            .claim(AUTHORITIES_KEY, authorities)
            .setExpiration(accessTokenExpiredTime)
            .signWith(key, SignatureAlgorithm.HS512)
            .compact()

        val refreshToken = Jwts.builder()
            .setExpiration(refreshTokenExpiredTime)
            .signWith(key, SignatureAlgorithm.HS512)
            .compact()

        return TokenBaseDto(accessToken = accessToken, refreshToken = refreshToken)
    }

    fun getAuthentication(token: String): Authentication {
        val claims = Jwts.parserBuilder()
            .setSigningKey(key)
            .build()
            .parseClaimsJws(token)
            .body

        val authorities = Arrays.stream(claims[AUTHORITIES_KEY]
            .toString()
            .split(&quot;,&quot;.toRegex()).dropLastWhile { it.isEmpty() }
            .toTypedArray())
            .map { role: String? -&amp;gt; SimpleGrantedAuthority(role) }
            .collect(Collectors.toList())

        val memberId = claims[&quot;memberId&quot;]

        val userDetails = customUserDetailsService.loadUserByUsername(memberId = memberId.toString())

        return UsernamePasswordAuthenticationToken(userDetails, &quot;&quot;, authorities)
    }

    fun validateToken(token: String): Boolean {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)
            return true
        } catch (e: io.jsonwebtoken.security.SecurityException) {
            log.error(&quot;잘못된 JWT 서명입니다.&quot;)
        } catch (e: MalformedJwtException) {
            log.error(&quot;잘못된 JWT 서명입니다.&quot;)
        } catch (e: ExpiredJwtException) {
            log.error(&quot;만료된 JWT 토큰입니다.&quot;)
        } catch (e: UnsupportedJwtException) {
            log.error(&quot;지원되지 않는 JWT 토큰입니다.&quot;)
        } catch (e: IllegalArgumentException) {
            log.error(&quot;JWT 토큰이 잘못되었습니다.&quot;)
        }

        return false
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 검증: 받아온 토큰을 서명정보와 만료 여부 지원 여부등을 판단하여 에러 로그를 찍고 있습니다. 이 때 false 를 반환하고 Exception을 반환 하지 않는 이유는 단순합니다. 인증이 필요 없는 요청도 반드시 JWT Filter를 타게 되는데 이때 인증이 필요 없는 경우에도 Exception으로 인해 서비스를 이용하지 못하는 경우를 방지하기 위함 입니다.&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;토큰 열람: 비밀키를 이용해 decode한 후 내부 claims 정보를 parsing 하여 열람합니다. 이 때 인증 및 인가 정보를 가져와 반환합니다.&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;그럼 이제 JWT 정의 부터 구현까지 모두 살펴 보았습니다. 그럼 마무리 Q&amp;amp;A를 하면서 마치도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리 Q &amp;amp; A&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q1: 폐기된 인증 정보로 해커가 인증을 시도한 경우는 어떡하나요?&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;보통 이런 경우에 대비하여 로그인 좌표나 인증 기기의 정보 등을 파악하여 비정상적인 로그인이라 판단될 경우 추가 인증을 요구하거나 본 사용자의 타 기기로 알림을 주는 방식으로 방지할 수 있습니다. 블랙 리스트를 사용해도 되지만 이는 stateful 하기 때문에 stateless라는 장점을 취하기 위한 목적으로 JWT를 생각하셨다면 JWT 사용 목적 자체가 모호해질 수 있습니다.&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;또한 session도 결국 session을 받아오기 위한 session ID 정보를 탈취하여 인증 시도가 가능하기 때문에 이러한 열람 가능한 정보는 유효시간을 짧게주어 해킹으로 부터 예방하는 것이 좋습니다.&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;Q2: 그러면 인증 되어있는 시간이 너무 짧아 UX가 안좋아지지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token의 짧은 유효 기간으로 인해 UX가 내려갈 수 있습니다. 이를 해결하기 위해 Refresh Token을 고려해보 수 있습니다. Refresh Token은 사용자 정보를 들고 있지 않은 유효시간이 긴 Token으로 만들어 Access Token 재발급이 가능합니다. 또는 인가 정보를 제외한 최소한의 정보를 담은 Refresh Token으로 만들 수도 있습니다.&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;Q3: MSA에서 JWT를 쓰면 유리한가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 따라 다르다고 말씀드리고 싶습니다. 인증 인가로 들어가는 정보가 적어 저장소의 도움없이 인증 인가를 처리할 수 있는 환경이면 stateless한 HTTP 프로토콜 특성을 이용하며 자유롭게 여러 서버에서 인증 인가를 할 수 있기 때문에 유리할 수 있지만 저장소를 다녀와야하는 상황이라면 여러 서버가 인증 인가를 저장소로 부터 받아와 검증하는 코드를 마구잡이로 넣어줘야하니 session과 크게 다를 것 없이 StateFul한 특성을 갖는 인증 방식이 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/ilgolf/blog_for_developer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/ilgolf/blog_for_developer&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716562624397&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - ilgolf/blog_for_developer: 개발자를 위한 게시판 서비스 toy project contribute 해주셔도 됩니다.&quot; data-og-description=&quot;개발자를 위한 게시판 서비스 toy project contribute 해주셔도 됩니다. Contribute to ilgolf/blog_for_developer development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ilgolf/blog_for_developer&quot; data-og-url=&quot;https://github.com/ilgolf/blog_for_developer&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cwnGxF/hyV90bFGO3/tJ0DA9DHDkyQuuKI6vfO5k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/ilgolf/blog_for_developer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ilgolf/blog_for_developer&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cwnGxF/hyV90bFGO3/tJ0DA9DHDkyQuuKI6vfO5k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - ilgolf/blog_for_developer: 개발자를 위한 게시판 서비스 toy project contribute 해주셔도 됩니다.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개발자를 위한 게시판 서비스 toy project contribute 해주셔도 됩니다. Contribute to ilgolf/blog_for_developer development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>블로그 프로젝트</category>
      <author>DEV_GOLF</author>
      <guid isPermaLink="true">https://golf-dev.tistory.com/89</guid>
      <comments>https://golf-dev.tistory.com/89#entry89comment</comments>
      <pubDate>Fri, 24 May 2024 23:57:22 +0900</pubDate>
    </item>
    <item>
      <title>Routify가 javascript file에서 동작하지 않는 이슈</title>
      <link>https://golf-dev.tistory.com/88</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;svelte 프로젝트에서 @roxi/routify 라이브러리를 활용하여 SPA를 구현하던 중 인증 실패 시 다시 root 페이지로 렌더링 하기위해 $goto function을 사용하였습니다. 코드로 살펴보면 다음과 같았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711260041564&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const handleError = async (error) =&amp;gt; { 
	const { config } = error;
    const refreshToken = localStorage.getItem('refreshToken');
    
    // ... other code
    
    // 실패 시 $goto('/') 로 root page 이동
    alert(error.response.data.body.message);
    $goto('/');
}&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;하지만 $goto function이 동작하지 않아 제가 예상한 root page로 이동이 아닌 alert만 발생하고 root page로 이동하지 않아 애를 먹던 상황입니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 @roxi/routify는 javascript library였기 때문에 당연히 동작할 수 있을 것 같지만 동작하지 않아 '$goto is not working in routify'라는 키워드로 검색 해보았습니다. 그리고 공식 Github issue에서 다음과 같은 글을 볼 수 있었습니다.&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;974&quot; data-origin-height=&quot;679&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzbpKw/btsF1Dc29ek/zUUX7bj6Lv4tC8VZrwUacK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzbpKw/btsF1Dc29ek/zUUX7bj6Lv4tC8VZrwUacK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzbpKw/btsF1Dc29ek/zUUX7bj6Lv4tC8VZrwUacK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzbpKw%2FbtsF1Dc29ek%2FzUUX7bj6Lv4tC8VZrwUacK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;974&quot; height=&quot;679&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;679&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 해석을 하자면 svelte component 외부에서는 $goto function을 사용할 수 없다는 내용이었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 사용하지 못했을까요? 정답은 내부 동작에 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711261225230&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const goto = {
    subscribe: (run, invalidate) =&amp;gt; {
        const { router } = contexts

        return derived(url, $url =&amp;gt;
            /** @type {Goto} */
            (pathOrNode, userParams, options) =&amp;gt; {
                const path =
                    typeof pathOrNode === 'string' ? pathOrNode : pathOrNode?.path

                /** @type {options} */
                const defaults = { mode: 'push', state: {} }
                options = { ...defaults, ...options }
                const newUrl = $url(path, userParams, options)
                router.url[options.mode](newUrl, options.state)
                return ''
            },
        ).subscribe(run, invalidate)
    },
}&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;해당 코드는 routify의 goto function의 구현 코드입니다. 내부적으로 store의 derived로 url을 기반으로 새로운 store를 파생시켜 url의 상태 변경에 따라 라우팅을 수행합니다. 이는 흔한 component간 통신 수단이며 store 자체는 반응형 상태 저장소로써 역할을 수행합니다. 그렇기 때문에 component 외부에서 이 동작을 수행하려고 하니 동작하지 않았던 것입니다.&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;그렇다면 왜 이렇게 설계된 것일까요? routify 진영에선 굳이 상태 관리를 활용할 이유가 있었을까요?&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;공식문서에서 따로 해당 이유가 설명된 부분을 찾진 못해서 GPT를 통해 유추 해봤을 땐 다음과 장점이 있다고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 반응성: Svelte 스토어는 상태 변화가 발생할 때마다 자동으로 구독 중인 컴포넌트를 업데이트합니다. 라우팅 상태가 변경될 때마다 관련된 뷰나 컴포넌트가 자동으로 반응하여 변경된 상태를 반영할 수 있습니다.&lt;br /&gt;&lt;br /&gt;2. 중앙 집중식 상태 관리: 라우팅 상태를 중앙 집중식으로 관리함으로써, 애플리케이션 내 어느 곳에서나 현재 라우트의 상태에 접근하고, 변경 사항을 쉽게 추적할 수 있습니다.&lt;br /&gt;&lt;br /&gt;3. 유연성과 확장성: 스토어를 사용하면 라우팅 로직을 애플리케이션의 다른 부분과 쉽게 연결할 수 있습니다. 예를 들어, 사용자 인증 상태에 따라 라우트 접근을 제어하는 등의 복잡한 라우팅 로직을 구현할 때 유용합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필자가 생각하는 이러한 설계 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 GPT에서 답변을 준 대로 반응성을 활용한 컴포넌트의 빠른 상태 반영을 통한 빠른 렌더링의 이점과 굳이 이런 이점을 포기하고 JS에서 동작하게 할 이유가 없다고 생각했습니다.&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;필자의 의견은 OOP에 기반한 사고에서 시작되었습니다. OOP에서는 기본적으로 하나의 class 또는 객체는 하나의 역할만 수행해야합니다. 그렇다면 현재 작성한 파일을 기반으로 역할을 나눠보면 다음과 같습니다.&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;error handling에 대한 책임&lt;/p&gt;
&lt;pre id=&quot;code_1711263386343&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { config } = error;
const refreshToken = localStorage.getItem('refreshToken');
    
// ... other code&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 이용해 화면에 나타내는 책임&lt;/p&gt;
&lt;pre id=&quot;code_1711263487638&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;alert(error.response.data.body.message);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;렌더링을 통해 화면을 제어하는 책임&lt;/p&gt;
&lt;pre id=&quot;code_1711263579034&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$goto('/');&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;하나의 function에서 너무 많은 일을 처리하고 있습니다. OOP 규칙을 위반하고 있고 그로 인하여 해당 서비스의 view 흐름을 파악하려면 불필요한 configuration code도 찾아봐야합니다. 그리고 view에 대한 역할을 하는 라이브러리들에 변경이 필요할 때도 책임이 흩어져있어 여러 layer를 손봐야할 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test 또한 관심사가 분리가 안되어 불필요한 맥락이 들어가 명확한 애플리케이션 스펙을 나타내지 못할 수도 있습니다.&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;이러한 이유로 js에서 동작하게 만들어도 애플리케이션의 책임이 분리되어 오히려 유지보수성을 떨어트릴 뿐 어떤 이익도 주지 못합니다. 결론적으로 js에서 동작을 하게 설계할 이유가 없는 것이죠.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 위 문제는 필자가 생각한 사고를 바탕으로 판단 했을 때 당연히 .svelte에서 동작하는 것이 맞다고 판단하였습니다. .svelte는 MVC 중에서 View를 담당하는 component 코드를 담고 있기 때문에 애플리케이션 동작을 처리하는 .js 파일에선 에러를 던지기만하고 실제로 에러를 받아 view에서 화면처리와 routing을 담당하게 하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 코드&lt;/p&gt;
&lt;pre id=&quot;code_1711264364539&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	const results = async (id) =&amp;gt; {
    	try {
        	return await getList(id);
        } catch(error) {
        	console.error('error : ' error);
            
            if (error.response.status === 401) {
            	alert('로그인이 필요합니다.');
                $goto('/');
            }
            
            $goto('/error');
        }
    }
&amp;lt;/script&amp;gt;

&amp;lt;div class=&quot;container&quot;&amp;gt;
   &amp;lt;!-- other code --&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해결 하였을 때 장점은 책임이 명확히 나뉘고 애플리케이션 동작에 문제가 생겼을 때 원인에 따라 유지보수할 코드가 확실히 갈리게 되었고 small test에서 router나 rendering 과 같은 화면과 관련된 동작을 전혀 신경쓸 이유가 사라져 훨씬 spec 명세가 명확해졌습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;etc. 만약 어쩔 수 없이 써야한다면?&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zuG6G/btsF4AeGJr6/v6LEuGB2DZGmIOBXvOAA91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zuG6G/btsF4AeGJr6/v6LEuGB2DZGmIOBXvOAA91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zuG6G/btsF4AeGJr6/v6LEuGB2DZGmIOBXvOAA91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzuG6G%2FbtsF4AeGJr6%2Fv6LEuGB2DZGmIOBXvOAA91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1054&quot; height=&quot;579&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 써야한다면 layout.svelte에 선언된 goto function을 globalGoto라는 writable 전역 변수에 넣어주고 JS에서 쓰게 해줄 순 있습니다. 다만 필자는 이러한 방식을 쓰기 전에 먼저 본인이 이 설계에 문제가 없는지 고려 후에 사용하는 것을 권장합니다. (모든 것엔 trade off가 있으니 모든 문제가 SRP를 지켜가며 풀리지 않을 수도 있습니다.)&lt;/p&gt;</description>
      <category>Front-End/javascript</category>
      <category>javascript</category>
      <category>routify</category>
      <category>Routing</category>
      <category>Svetle</category>
      <author>DEV_GOLF</author>
      <guid isPermaLink="true">https://golf-dev.tistory.com/88</guid>
      <comments>https://golf-dev.tistory.com/88#entry88comment</comments>
      <pubDate>Sun, 24 Mar 2024 16:37:08 +0900</pubDate>
    </item>
    <item>
      <title>Svelte란?</title>
      <link>https://golf-dev.tistory.com/87</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;svelte는 UI를 그리기 위한 javascript 기반 framework로 2016년 비교적 최근에 출시되었습니다. 이 framework는 많은 사랑을 받고 있기도 한데요. state Js of 2023 사이트의 자료에 의하면 매우 높은 비율로 흥미를 보이고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;727&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by4zhH/btsEY8qWYVs/OxUs44hHcxFwjJX6ELYz61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by4zhH/btsEY8qWYVs/OxUs44hHcxFwjJX6ELYz61/img.png&quot; data-alt=&quot;state JS of 2023 통계 자료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by4zhH/btsEY8qWYVs/OxUs44hHcxFwjJX6ELYz61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby4zhH%2FbtsEY8qWYVs%2FOxUs44hHcxFwjJX6ELYz61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1458&quot; height=&quot;727&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;727&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;state JS of 2023 통계 자료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 svelte에 대해 알아볼까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;real-dom 기반의 framework&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 react나 vue.js 같은 대중적인 js framework는 대다수가 virtual dom 기반의 spa를 지원합니다. 이유는 간단한데요 DOM을 경량화하여 가상으로 존재하게 하여 실제 DOM을 렌더링하는 것 보다 성능상으로 이점이 많기 때문입니다. VDOM은 전체 UI를 VDOM에 재렌더링하고 이전 VDOM과 비교하여 변경된 부분만 실제 VDOM에 적용합니다. 이러한 과정을 통해 DOM 조작을 최소화 했었습니다.&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;하지만 VDOM 또한 다음과 같은 단점이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오버헤드: VDOM을 사용할 경우 새로운 가상 DOM 트리를 생성하고 이를 이전 트리와 비교하여 변경사항을 탐색합니다. 하지만 이 과정은 변화가 많이 없음에도 전체 트리를 재생성하고 비교해야하기 때문에 변경이 적은 경우 오버헤드 비용이 발생합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;메모리 사용: VDOM은 메모리 상에 전체 DOM 트리의 복사본을 유지합니다. 이는 애플리케이션 규모에 따라 더 많은 메모리를 사용하여 리소스에 민감한 환경에서는 단점이 될 수 있습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Svelte는 이 문제를 해결하기 위하여 UI 컴포넌트를 최적화된 JS 코드로 변환하는 접근 방식을 이용해 해결하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;런타임 오버헤드 감소: VDOM을 더 이상 사용하지 않습니다. 대신, 컴파일 타임에 애플리케이션의 구조를 분석하여 필요한 DOM 업데이트를 수행할 최소한의 코드를 생성합니다. 이는 불필요한 코드 계산을 줄이고, 런타임 성능을 향상시킵니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;메모리 최적화: VDOM과 다르게 메모리에 가상 트리를 유지하지 않습니다. 때문에 메모리 효율성이 증대합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;코드량 감소: 상태 변경이 발생할 때 필요한 코드만 실행하여 DOM을 업데이트하기 때문에 코드량이 react나 vue.js에 비해 현저히 줄어듭니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 코드량 감소는 생산성과 이어지기 때문에 더욱 매력적으로 느껴집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Login.svelte&lt;/p&gt;
&lt;pre id=&quot;code_1708158083603&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  let email = '';
  let password = '';

  const login = async() =&amp;gt; {
     // 로그인 로직
     console.log(email, password);
  };
&amp;lt;/script&amp;gt;

&amp;lt;form on:submit|preventDefault={login}&amp;gt;
  &amp;lt;input type=&quot;email&quot; bind:value={email} placeholder=&quot;Email&quot;&amp;gt;
  &amp;lt;input type=&quot;password&quot; bind:value={password} placeholder=&quot;Password&quot;&amp;gt;
  &amp;lt;button type=&quot;submit&quot;&amp;gt;Login&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;login.jsx&lt;/p&gt;
&lt;pre id=&quot;code_1708158186840&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState } from 'react';

function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const login = async(e) =&amp;gt; {
    e.preventDefault();
    
    // 로그인 로직
    console.log(email, password);
  };

  return (
    &amp;lt;form onSubmit={login}&amp;gt;
      &amp;lt;input type=&quot;email&quot; value={email} onChange={(e) =&amp;gt; setEmail(e.target.value)} placeholder=&quot;Email&quot; /&amp;gt;
      &amp;lt;input type=&quot;password&quot; value={password} onChange={(e) =&amp;gt; setPassword(e.target.value)} placeholder=&quot;Password&quot; /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;Login&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}

export default Login;&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;Login.vue&lt;/p&gt;
&lt;pre id=&quot;code_1708158297489&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;form @submit.prevent=&quot;login&quot;&amp;gt;
    &amp;lt;input type=&quot;email&quot; v-model=&quot;email&quot; placeholder=&quot;Email&quot;&amp;gt;
    &amp;lt;input type=&quot;password&quot; v-model=&quot;password&quot; placeholder=&quot;Password&quot;&amp;gt;
    &amp;lt;button type=&quot;submit&quot;&amp;gt;Login&amp;lt;/button&amp;gt;
  &amp;lt;/form&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
export default {
  data() {
    return {
      email: '',
      password: ''
    };
  },
  methods: {
    login() {
      // 로그인 로직
      console.log(this.email, this.password);
    }
  }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 싶이 확연히 svelte의 코드가 적은 것을 확인할 수 있습니다. 이는 개발자 생산성에 큰 영향을 끼칩니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내가 Svelte를 선택한 이유 (feat. Server Engineer 에게 추천하는 이유)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그를 읽어오신 여러분들은 아실겁니다. 제가 Server Engineer라는것을요. 그럼 저는 svelte를 어쩌다 선택하게된 걸 까요?&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;정답은 비교적 익숙한 코드를 짤 수 있다는 장점입니다. 한 번 우리가 자주 쓰는 thymleaf 코드를 보실까요?&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708158711725&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Login&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;form th:action=&quot;@{/login}&quot; method=&quot;post&quot; id=&quot;loginForm&quot;&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;email&quot;&amp;gt;Email:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;email&quot; id=&quot;email&quot; name=&quot;email&quot; required /&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;password&quot;&amp;gt;Password:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot; required /&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;button type=&quot;submit&quot;&amp;gt;Login&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;

    &amp;lt;script&amp;gt;
        document.getElementById(&quot;loginForm&quot;).addEventListener(&quot;submit&quot;, function(event) {
            var email = document.getElementById(&quot;email&quot;).value;
            var password = document.getElementById(&quot;password&quot;).value;
            if (email.trim() === &quot;&quot; || password.trim() === &quot;&quot;) {
                alert(&quot;Email and password are required.&quot;);
                event.preventDefault(); // 폼 제출을 방지하여 서버로 전송되지 않게 합니다.
            }
            // 여기에 추가적인 클라이언트 측 유효성 검사 로직을 구현할 수 있습니다.
        });
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어딘가 닮은 점이 없나요? 네 맞습니다. &amp;lt;script&amp;gt;에 js 문법을 정의하고 js를 이용하여 html에 입력된 email password로 서버에 로그인 요청을 보냅니다. svelte도 매우 유사한 구조를 띄고 있죠. 그렇기 때문에 필자는 꽤 쉽게 svelte에 적응할 수 있었습니다. 또한 svelte는 SPA를 달성할 수 있기 때문에 코드량이 thymleaf 같은 html 템플릿 엔진보다 훨씬 감소하고 빠른 개발이 가능해집니다. 재사용 또한 가능합니다.&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;이번 포스팅에선 svelte를 왜쓰는지 svelte에 대한 소개였다면 다음엔 svelte의 컴파일 과정과 문법등 좀 더 기술 소개에 가까운 포스팅으로 돌아오겠습니다. 읽어주셔서 감사합니다!&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;출처.&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://svelte.dev/docs/introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://svelte.dev/docs/introduction&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1708159009414&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;Introduction &amp;bull; Docs &amp;bull; Svelte&quot; data-og-description=&quot;Edit this page on GitHub On this page On this page Welcome to the Svelte reference documentation! This is intended as a resource for people who already have some familiarity with Svelte and want to learn more about using it. If that's not you (yet), you ma&quot; data-og-host=&quot;svelte.dev&quot; data-og-source-url=&quot;https://svelte.dev/docs/introduction&quot; data-og-url=&quot;https://svelte.dev/docs/introduction&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ykyFJ/hyVjjX28pn/zPK1CBlBE8XKnNALJ4T0c1/img.jpg?width=640&amp;amp;height=640&amp;amp;face=0_0_640_640&quot;&gt;&lt;a href=&quot;https://svelte.dev/docs/introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://svelte.dev/docs/introduction&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ykyFJ/hyVjjX28pn/zPK1CBlBE8XKnNALJ4T0c1/img.jpg?width=640&amp;amp;height=640&amp;amp;face=0_0_640_640');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Introduction &amp;bull; Docs &amp;bull; Svelte&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Edit this page on GitHub On this page On this page Welcome to the Svelte reference documentation! This is intended as a resource for people who already have some familiarity with Svelte and want to learn more about using it. If that's not you (yet), you ma&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;svelte.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://2022.stateofjs.com/en-US/libraries/front-end-frameworks/#front_end_frameworks_experience_linechart&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://2022.stateofjs.com/en-US/libraries/front-end-frameworks/#front_end_frameworks_experience_linechart&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1708159018514&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;State of JavaScript 2022: Front-end Frameworks&quot; data-og-description=&quot;This chart splits positive (&amp;ldquo;want to learn&amp;rdquo;, &amp;ldquo;would use again&amp;rdquo;) vs negative (&amp;ldquo;not interested&amp;rdquo;, &amp;ldquo;would not use again&amp;rdquo;) experiences on both sides of a central axis. Bar thickness represents the number of respondents aware of a technology.&quot; data-og-host=&quot;2022.stateofjs.com&quot; data-og-source-url=&quot;https://2022.stateofjs.com/en-US/libraries/front-end-frameworks/#front_end_frameworks_experience_linechart&quot; data-og-url=&quot;https://2022.stateofjs.com//en-US/libraries/front-end-frameworks/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b4ZYdI/hyVi9A8ZSV/1pV37y44knbhdR2wQKUrJ0/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675&quot;&gt;&lt;a href=&quot;https://2022.stateofjs.com/en-US/libraries/front-end-frameworks/#front_end_frameworks_experience_linechart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://2022.stateofjs.com/en-US/libraries/front-end-frameworks/#front_end_frameworks_experience_linechart&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b4ZYdI/hyVi9A8ZSV/1pV37y44knbhdR2wQKUrJ0/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;State of JavaScript 2022: Front-end Frameworks&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This chart splits positive (&amp;ldquo;want to learn&amp;rdquo;, &amp;ldquo;would use again&amp;rdquo;) vs negative (&amp;ldquo;not interested&amp;rdquo;, &amp;ldquo;would not use again&amp;rdquo;) experiences on both sides of a central axis. Bar thickness represents the number of respondents aware of a technology.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;2022.stateofjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Front-End/svelte</category>
      <category>Front-end</category>
      <category>javascript</category>
      <category>React</category>
      <category>svelte</category>
      <category>개발자</category>
      <author>DEV_GOLF</author>
      <guid isPermaLink="true">https://golf-dev.tistory.com/87</guid>
      <comments>https://golf-dev.tistory.com/87#entry87comment</comments>
      <pubDate>Sat, 17 Feb 2024 17:37:27 +0900</pubDate>
    </item>
    <item>
      <title>OOM이 발생했을 때 어버버 하지 않게 대비하기!</title>
      <link>https://golf-dev.tistory.com/86</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 업무 중 OOM이 발생하며 트러블 슈팅을 하면서 배웠던 내용들을 공유드릴려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OOM이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;out of memory의 앞글자에서 따온 말로 heap 영역에 인스턴스나 더미 데이터들이 정해놓은 heap 크기보다 커졌을 때 보통 발생합니다. 또는 스레드 스택 메모리나 메타 스페이스 등에 데이터가 꽉차더라도 문제가 발생할 수 있습니다. 더 이상 인스턴스에게 새로운 메모리 공간을 할당할 수 없기 때문에 서버는 에러를 내고 서버를 죽입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 OOM이 발생하는 코드를 만들어봤습니다. 이 코드에선 계속해서 ArrayList에 데이터를 append하여 추가하다가 어느 순간 메모리 공간이 부족해지면 에러가 발생하여 프로그램이 동작을 멈출 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1695742328522&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {

    val list = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    val newList = mutableListOf&amp;lt;Int&amp;gt;()

    while (true) {
        newList.addAll(list)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 OOM의 원인은 발생하자마자 서버가 내려가기 때문에 해당 상황을 다시 재현하기 쉬운게 아니라면 찾기가 힘들 뿐더러 디버깅으로 찾기도 힘들기 때문에 보통 heap dump를 추출하여 분석해야합니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;heap dump?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;heap dump란 JVM의 heap 공간에 대한 스냅샷 정보입니다. heap 스냅샷 정보엔 보통 메모리 사이즈, 콘텐츠, 가비지 컬렉션 루트 등의 정보들이 포함되어있고 보통 이 내용들을 분석하여 어떤 객체가 heap 공간을 많이 차지하고 있는지 제거되지 않고 계속 남아있는지 찾을 수 있습니다.&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;보통 hporf 파일 확장자를 갖고 있으며 메모리 상태를 그대로 반영하기 때문에 파일 크기가 큽니다. 다양한 분석 도구가 있는데 대표적으로 MAT이나 VisualVM등을 사용해볼 수 있습니다.&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;하지만 oom이 발생했을 땐 이미 서버가 내려간 시점이고 heap dump를 분석하기에도 이미 늦은 상황입니다. 그러면 우리는 결국 이 상황을 재현할 때 까지 기다렸다가 서버 메모리나 GC time에 문제가 발생했을 때 heap dump를 분석하여 해결해야합니다. 하지만 마냥 기다리기엔 리스크가 크죠.&amp;nbsp; 그러지 않기 위해선 대비책이 필요해보입니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어버버 하지 않기 위한 첫 번째 방법 (VisualVM 이용하기)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;visualVM을 이용하여 외부 서버에 접속 시킨 뒤 해당 서버가 OOM이 발생했을 때 원격 서버에 연결을 해뒀다면 해당 서버 heap dump가 기록 되어있기 때문에 분석하여 OOM의 원인을 찾을 수 있습니다.&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;3448&quot; data-origin-height=&quot;1748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q5P8l/btsvNtE3POd/k6VoyIngG0SxSWk2iQQdHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q5P8l/btsvNtE3POd/k6VoyIngG0SxSWk2iQQdHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q5P8l/btsvNtE3POd/k6VoyIngG0SxSWk2iQQdHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ5P8l%2FbtsvNtE3POd%2Fk6VoyIngG0SxSWk2iQQdHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3448&quot; height=&quot;1748&quot; data-origin-width=&quot;3448&quot; data-origin-height=&quot;1748&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 visualVM의 UI 화면입니다. remote 연결도 가능하기 때문에 원격 서버에 heap dump 분석도 가능합니다. 위에 보이는 숫자들이 byte 단위의 용량입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해당 프로젝트는 생각보다 상세하게 분석되는 느낌이 부족해서 사용하진 않았습니다. 또한 원격 서버와 연결하는 것도 불편했습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;어버버 하지 않기 위한 두 번째 방법 (MAT or IntelliJ 이용하기)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자가 사용했던 방법입니다. UI가 깔끔하고 직관적인 분석이 가능했습니다. 또한 JVM 옵션만 잘 준다면 MAT을 이용하여 정확한 OOM 원인 분석이 가능했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 옵션이 하나 필요합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1695744800938&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 옵션은 JVM 실행시 java 명령어 뒤에 넣어주면 됩니다. 이렇게 옵션을 넣어준 뒤 애플리케이션을 실행한다면 OOM이 발생하였을 때 저절로 heap dump가 생성됩니다.&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;이렇게 만들어진 heap dump를 MAT이나 intelliJ로 열어서 분석하면 되는데 MAT 기준으로 이러한 UI 창을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V0MO2/btsvMICspkv/ABkOPf43aqRp4POjAauka0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V0MO2/btsvMICspkv/ABkOPf43aqRp4POjAauka0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V0MO2/btsvMICspkv/ABkOPf43aqRp4POjAauka0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV0MO2%2FbtsvMICspkv%2FABkOPf43aqRp4POjAauka0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;779&quot; height=&quot;589&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 사진은 실제 JDK 17과 scouter간에 호환성 문제로 인하여 OOM이 발생한 프로그램을 heap dump를 떠서 분석한 내용입니다. 보면 scouter.javaasist.ClassPoolTail 이라는 객체가 약 87% 이상을 차지하고 있고 이 인스턴스로 인해 OOM이 발생했다고 알려주고 있습니다.&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;657&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2mT9S/btsvYZ3oRx6/ckl5hnYqkxPTiP1oLlmSck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2mT9S/btsvYZ3oRx6/ckl5hnYqkxPTiP1oLlmSck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2mT9S/btsvYZ3oRx6/ckl5hnYqkxPTiP1oLlmSck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2mT9S%2FbtsvYZ3oRx6%2Fckl5hnYqkxPTiP1oLlmSck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;904&quot; height=&quot;657&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;657&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글에 검색해보니 바로 OOM 발생 이슈가 올라와있는 것을 확인할 수 있습니다. (실제로 현재 문제는 이미 scouter 측에서 문제를 해결한 버전을 출시해 놨습니다. scouter 최고 !!)&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;이러한 직관적인 UI를 기반으로 필자는 비교적 쉽게 OOM이 발생하는 원인을 찾았고 해결하였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OOM이 발생했을 때 어버버 하지 않기에 대한 내용을 주제로 글을 적어봤는데요. 가장 중요한 것은 대비 입니다. 필자는 나중에 JVM의 OOM시 heap dump를 뜨는 옵션을 뒤늦게 넣어주고 운영 테스트 해가면서 찾아냈지만 만약 미리 저 옵션을 넣어놓고 언제든 OOM이 발생하더라도 heap dump를 분석할 수 있게 대비해놓았으면 더 빠르게 이슈 트래킹이 가능했을 겁니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꼭 여러분들도 이러한 이슈를 그냥 해결하고 넘기지 말고 이러한 문제를 다신 발생하지 않게 관리하는 것이 중요하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실수는 방지하는 것이 아닌 관리하는 것이다.&lt;br /&gt;&lt;/span&gt;- 함께 자라기 (김창준 지음)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침.&lt;/p&gt;</description>
      <category>Java</category>
      <category>heap</category>
      <category>java</category>
      <category>JVM</category>
      <category>Kotlin</category>
      <category>memory</category>
      <category>Spring</category>
      <category>트러블 슈팅</category>
      <author>DEV_GOLF</author>
      <guid isPermaLink="true">https://golf-dev.tistory.com/86</guid>
      <comments>https://golf-dev.tistory.com/86#entry86comment</comments>
      <pubDate>Wed, 27 Sep 2023 01:22:41 +0900</pubDate>
    </item>
    <item>
      <title>docker storage driver를 신중하게 선택하자!!</title>
      <link>https://golf-dev.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;jenkins로 CI를 하던 와중 jenkins 서버 내 용량이 이상하리 만큼 많이 쌓여서 원인을 분석하면서 storage driver에 대해 알게 됐습니다. storage driver를 잘 알고 있지 못하면 생기는 문제 그리고 storage driver 종류에 따라 생기는 문제를 알아보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Storage Driver란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker storage driver는 docker image로 부터 container를 생성하는데 이 때 docker image는 application code, system, library등을 포함합니다. 그리고 이를 저장하고 관리하기 위해선 store system이 필요한데 이것을 storage driver 이라고 합니다.&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;762&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFL8Aw/btsvncIPLT0/QljSl7aGmdDAdxukaS6SyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFL8Aw/btsvncIPLT0/QljSl7aGmdDAdxukaS6SyK/img.png&quot; data-alt=&quot;storage engine 도식도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFL8Aw/btsvncIPLT0/QljSl7aGmdDAdxukaS6SyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFL8Aw%2FbtsvncIPLT0%2FQljSl7aGmdDAdxukaS6SyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;426&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;storage engine 도식도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, storage driver는 image가 여러 레이어로 구성된 것 처럼 레이어를 효율적으로 저장하기 위해 병합 및 추출하는 역할을 합니다. Copy-on-Write 전략을 사용하여 새 컨테이너를 시작할 때 마다 전체 이미지를 복사하는 것이 아닌 변경된 부분만 디스크에 기록하여 디스크를 아껴사용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이러한 좋은 사용성도 결국 개발자가 어떻게 활용하냐에 따라 천차 만별입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠시 예시를 보고 문제가 있는 Dockerfile입니다. 한 번 문제를 찾아봅시다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1695386150655&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;## Dockerfile
FROM openjdk:17-alpine

USER root

ARG SCOUTER_GIT_HUB_URL

RUN apk add curl &amp;amp;&amp;amp; mkdir &quot;scouter&quot;;

RUN curl -o /scouter/scouter-agent.java.tar.gz $SCOUTER_GIT_HUB_URL

RUN rm -rf /scouter/scouter-agent.java.tar.gz

ADD build/libs/blog.jar build/libs/blog.jar

EXPOSE 8099&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잠시 생각해봅시다 ....&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;뭐가 문제일까?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아까 우리는 Storage driver가 여러 레이어를 효율적으로 저장하기 위해 병합 및 추출을 하고 변경된 레이어만 디스크에 기록한다고 했습니다. 그럼 다시 살펴보죠 현재 RUN 명령어 레이어만 3개가 있습니다. 이 경우 스토리지 엔진은 다음과 같이 쌓을 것입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Alpine-Linux OS 디렉토리 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;openJDK가 추가된 레이어에 대한 디렉토리 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apk curl 패키지가 추가된 레이어에 대한 디렉토리 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scouter를 다운로드 받아 생겨난 레이어 디렉토리 생성&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scouter를 삭제한 후 생겨난 레이어 디렉토리 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;blog.jar 파일이 추가되며 생겨난 레이어 디렉토리 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞습니다. 너무 많은 레이어가 생겨나면서 폴더가 너무 많이 생겨났고 나중에 보겠지만 overlay2 storage라면 그나마 디렉토리간 파일을 공유하기 때문에 중복된 파일이 덜 발생하겠지만 vfs storage driver는 그렇지 않기 때문에 대단히 많은 용량이 storage에 쌓일 것입니다.&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;그리고 overlay2를 사용하더라도 용량을 효율적으로 쓰는 것은 아니기 때문에 당연히 주의하여야합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 어떻게 개선해볼 수 있을까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RUN 레이어를 하나로 만드는 것입니다. RUN 간에 발생하는 명령어를 \ 를 이용해서 합칠 수 가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1695387290230&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;## Dockerfile
FROM openjdk:17-alpine

USER root

ARG SCOUTER_GIT_HUB_URL

RUN apk add curl &amp;amp;&amp;amp; mkdir &quot;scouter&quot;; \
    curl -o /scouter/scouter-agent.java.tar.gz $SCOUTER_GIT_HUB_URL &amp;amp;&amp;amp; \
    rm -rf /scouter/scouter-agent.java.tar.gz &amp;amp;&amp;amp; \
    
ADD build/libs/blog.jar build/libs/blog.jar

EXPOSE 8099&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선된 Dockerfile은 위와 같습니다. RUN이 하나의 레이어에서 모든 명령어를 처리하기 때문에 명령어가 끝난 직후의 스냅샷이 디스크에 기록될 것입니다. 그리고 디렉토리는 총 3개만 생기기 때문에 적은 용량이 쌓이게 됩니다.&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;그럼 모든 용량이 개선된걸 까요? 사실 overlay2 storage driver를 사용했다면 여기서 추가적인 개선이 필요 없습니다. 하지만 vfs storage driver는 다릅니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이 둘의 차이를 알아봅시다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Overlay2 vs VFS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 각각의 특징을 살펴보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;overlay2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;overlay2는 overlayFS 파일 시스템을 이용하여 여러 레이어를 하나의 뷰로 관리합니다. Copy-on-write 전략을 사용하기 때문에 공간을 효율적으로 관리하며 하나의 뷰로 관리하기 때문에 storage에 저장된 디렉토리는 하나의 이미지 크기랑 유사합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker에서 권장하는 storage driver이며 성능 또한 storage driver 중 꽤 높은 성능을 자랑합니다.&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;다만, linux 커널 4.0 이하에선 사용할 수 없으며 CentOS 7.6 버전 이상에서만 사용이 가능하여 비교적 호환이 안되는 경우가 발생할 수 있습니다.&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;vfs&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vfs는 여러 레이어를 하나의 뷰로 관리하지 않씁니다. 각 레이어는 별도의 디렉토리로 저장됩니다. 성능이 낮은 편이며 공간 효율성 또한 Image의 각 레이어를 독립된 디렉토리로 관리하기 때문에 중복 파일이 발생하여 공간을 낭비할 수 있습니다.&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;다만, CentOS 낮은 버전이나 linux 낮은 버전에서도 호환이 되며 호환성으로 인한 문제는 발생하지 않습니다.&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;결론적으로 말씀드리면 vfs를 사용하는 것은 한계가 있습니다. 위 같은 이미지도 host에는 결국 무거운 용량이 쌓이게 됩니다. 아무리 레이어를 최대한 줄여 쌓이는 양을 줄여도 위 같은 경우 OS만 총 4번 무거워 지며 중복되어 저장되기 때문에 비효율적입니다.&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;물론 이 또한 최적화가 가능합니다. 실제로 필자는 호환성 문제 때문에 vfs로 갈 수 밖에 없었고 최대한 Dockerfile의 레이어를 가볍게 쌓기 위하여 다음과 같이 개선했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1695389282707&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;## Dockerfile
FROM alpine:3.18.3

USER root

ARG SCOUTER_GIT_HUB_URL

ADD build/libs/blog.jar build/libs/blog.jar

RUN apk add curl openJdk17 &amp;amp;&amp;amp; mkdir &quot;scouter&quot;; \
    curl -o /scouter/scouter-agent.java.tar.gz $SCOUTER_GIT_HUB_URL &amp;amp;&amp;amp; \
    rm -rf /scouter/scouter-agent.java.tar.gz


EXPOSE 8099&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Alpine-Linux는 5MB에 적은 용량을 자랑하는 경량 Linux입니다. 그렇기 때문에 먼저 OS만 추가한 후 여기에 가장 무거울 것이라 예상되는 JDK와 curl을 한 번에 RUN 레이어에 넣어줍니다. 또한 jar 파일 복사 시점을 RUN 전으로 하여 최대한 무거운 디렉토리가 마지막에 쌓이게 유도합니다.&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;다음과 같은 최적화로 약 279MB의 curl + jdk가 들어간 디렉토리를 마지막에 쌓아 빌드 시 발생하는 이미지 storage&amp;nbsp; 총 용량을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;750MB -&amp;gt; 400MB 정도로 줄였습니다.&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;하지만 이 경우도 Alpine-Linux를 사용했을 때 상황입니다. OS 안정성을 위해 alpine-linux보다 debian을 선호한다면 vfs 스토리지는 아마 한 번 빌드 할 때 마다 1GB 정도의 디렉토리가 쌓일 것입니다.&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;결국 가장 좋은 방법은 호환 되는 OS로 업그레이드 하여 overlay2로 넘어가는 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 문제 해결에 정답은 없는 것 같습니다. 저 같은 경우엔 당장 서버 centOS 버전을 올리는 것은 무리라고 판단 Dockerfile을 최소화하여 빌드하고 추후 너무 많은 용량이 쌓이면 docker system prune 같은 dangling image와 storage를 비워주는 명령어로 용량을 주기적으로 비워주기로 하였습니다.&amp;nbsp;&lt;br /&gt;이는 CI 만을 하기 위한 Jenkins 서버여서 가능했습니다. 빌드만 일어나기 때문에 중요한 docker volume이나 network 정보가 없기 때문입니다.&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;하지만 예를들어 운영중인 서버라면 CentOS 버전업이 가장 좋은 솔루션일 수 있습니다. docker system prune은 운영 중인 서비스 서버에선 다소 위험한 명령어이기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 환경 고려를 꼭 해봅시다!&lt;/p&gt;</description>
      <category>docker</category>
      <category>DevOps</category>
      <category>docker</category>
      <category>Infra</category>
      <category>java</category>
      <category>면접</category>
      <category>백엔드</category>
      <category>인터뷰</category>
      <category>컨테이너</category>
      <author>DEV_GOLF</author>
      <guid isPermaLink="true">https://golf-dev.tistory.com/85</guid>
      <comments>https://golf-dev.tistory.com/85#entry85comment</comments>
      <pubDate>Fri, 22 Sep 2023 22:37:17 +0900</pubDate>
    </item>
    <item>
      <title>BigDecimal이라는 라이브러리가 존재하는 이유는 뭘까?</title>
      <link>https://golf-dev.tistory.com/84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JVM 기반의 언어를 사용할 때 보통 돈 같은 필드는 타입을 BigDecimal로 사용하는 경우가 많습니다. 그리고 필자도 개인 프로젝트를 하면서 돈과 관련된 필드는 무조건 BigDecimal을 사용했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유가 궁금해서 찾아보면서 깊게 알아본 내용들을 바탕으로 정리해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부동 소수점이란?&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigDecimal을 쓰게된 이유를 찾기 위해선 부동 소수점이라는 개념을 먼저 알고 있어야합니다. 보통 PC에서 실수를 표현하고 연산할 때 소수점의 위치가 고정되어있는 것이 아닌 움직일 수 있는 경우를 부동 소수점이라고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IEEE 754 표준으로 보면 다음과 같이 표현할 수 있습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부호 : 1비트&amp;nbsp;&lt;/li&gt;
&lt;li&gt;지수 : 8비트&amp;nbsp;&lt;/li&gt;
&lt;li&gt;가수 : 23비트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배정 밀도의 경우&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부호 : 1비트&lt;/li&gt;
&lt;li&gt;지수 : 11비트&lt;/li&gt;
&lt;li&gt;가수 : 52비트&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/83LyT/btstvSzWfVN/yFMp31Bqqk3iQFhFMG0Db0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/83LyT/btstvSzWfVN/yFMp31Bqqk3iQFhFMG0Db0/img.png&quot; data-alt=&quot;출처 :&amp;amp;nbsp;https://steemit.com/kr/@modolee/floating-point&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/83LyT/btstvSzWfVN/yFMp31Bqqk3iQFhFMG0Db0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F83LyT%2FbtstvSzWfVN%2FyFMp31Bqqk3iQFhFMG0Db0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;689&quot; height=&quot;257&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://steemit.com/kr/@modolee/floating-point&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 부호는 음수/양수를 표현, 가수는 실제 숫자 값을 표현 그리고 지수는 소수점의 위치를 결정합니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부동 소수점의 한계?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부동소수점에 대해서 알아보았습니다. 근데 여기서 문제점을 발견하신 분도 계실겁니다. 배정 밀도로 표현한다 한들 최대 52비트까지만 숫자를 표현할 수 없다는 겁니다.&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;대표적인 예로 0.1(10진수)를 2진수로 표현해볼가요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;74&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p4K8L/btstwXU5xxF/fhZC7KAwxkXSRvo5oQMyK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p4K8L/btstwXU5xxF/fhZC7KAwxkXSRvo5oQMyK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p4K8L/btstwXU5xxF/fhZC7KAwxkXSRvo5oQMyK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp4K8L%2FbtstwXU5xxF%2FfhZC7KAwxkXSRvo5oQMyK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1288&quot; height=&quot;74&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;74&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속해서 무한이 반복되는 하나의 이진수가 생성됩니다. 그리고 부동 소수점은 이걸 52비트까지만 표현하겠죠. 그러면 숫자는 부정확한 값이 나타납니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694174792158&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {

    var number: Double = 0.0

    for (i in 1 .. 100) {
        number += 0.1
    }

    println(number)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드는 0.1을 100번 더하는 연산입니다. 연산을 하면 당연히 100번 0.1을 더했으므로 10이 나와야합니다. 하지만 결과 값은 다음과 같습니다.&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;1522&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSihS5/btstzwoDKk1/LMaYJHIq6DOugzoUghvQaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSihS5/btstzwoDKk1/LMaYJHIq6DOugzoUghvQaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSihS5/btstzwoDKk1/LMaYJHIq6DOugzoUghvQaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSihS5%2FbtstzwoDKk1%2FLMaYJHIq6DOugzoUghvQaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1522&quot; height=&quot;226&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;226&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;이것이 바로 부동 소수점의 한계를 나타내는 대표적인 예입니다. 이진 표현법으로 소수점을 제대로 표현할 수 없기 때문에 double형인 number 자료형은 52비트까지만 표현을 했고 그 과정에서 값이 변질되어 올바른 값대로 연산되지 않았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 그럼 어떻게 표현되고 있을까요?&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1694175115806&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    val number = 0.1
    val bits = java.lang.Double.doubleToLongBits(number)
    val binaryRepresentation = bits.toString(2).padStart(64, '0')

    println(&quot;binary representation: $binaryRepresentation&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;binary&amp;nbsp;representation:&amp;nbsp;0011111110111001100110011001100110011001100110011001100110011010&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한히 반복되는 수여야 하지만 그렇지 않고 일부만 표현되고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 은행 시스템이었다면 소비자들은 부동 소수점으로인해 본인이 원하는 금액보다 낮거나 높은 금액을 반환받을 것이며 이는 쌓이면 현금을 관리하는 입장에선 매우 치명적일 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그럼 어떻게 해결해야할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 BigDecimal을 쓰는 이유가 나옵니다. 문제를 되살펴보면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 부동 소수점은 이진 표현법으로 무한히 이어질 때 특정 비트까지만 표현하여 올바른 값을 표현 못한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 이로인해 연산이 부정확하게 일어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위한 가장 빠른 방법은 십진수로 연산하는 방법입니다. 하지만 컴퓨터는 이진 표현법 밖에 모릅니다. 그래서 JVM단에서는&amp;nbsp; BigDecimal을 지원합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BigDecimal이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigDecimal은 십진수를 표현하고 연산할 수 있는 라이브러리로 주요 목적은 부동 소수점에서 발생하는 근사값으로 인한 오차를 십진 연산을 수행함으로써 해결하는 것입니다.&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;BigDecimal은 다음과 같은 방법으로 문제를 풀어내고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정수 값과 정밀도를 나눠서 저장 (예를 들어 0.1의 경우 숫자를 정수로 표현하는 정수 값 1과 소수점 이하의 자릿수를 표현하는 정밀도 1로 나눠서 저장합니다.)&lt;/li&gt;
&lt;li&gt;연산 정수 값에 연산을 수행하고 정밀도를 적용하여 최종 결과를 나타내어 정확한 계산을 유도합니다.&lt;/li&gt;
&lt;li&gt;십진 표현을 사용하기 때문에 0.1을 정확하게 0.1로 표현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;불변성이 존재하기 때문에 Thread safe하게 정확한 연산을 수행합니다. 다만 이 경우 성능이 좀 떨어집니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 예시는 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694176202735&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.math.BigDecimal

fun main() {
    var number: BigDecimal = BigDecimal(&quot;0.0&quot;)

    for (i in 1 .. 100) {
        number = number.add(BigDecimal(&quot;0.1&quot;))
    }

    println(number)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNuOx0/btstvQ90QHa/VmxEPufHgck6kjO6GSnjY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNuOx0/btstvQ90QHa/VmxEPufHgck6kjO6GSnjY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNuOx0/btstvQ90QHa/VmxEPufHgck6kjO6GSnjY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNuOx0%2FbtstvQ90QHa%2FVmxEPufHgck6kjO6GSnjY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1434&quot; height=&quot;326&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 정확히 10이 나왔습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 원리를 이용하여 정확하게 소수점을 정확하게 계산해내고 부동 소수점처럼 문제가 발생하지 않습니다. 고로 우리는 금액같은 데이터도 안전하게 데이터를 관리할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부동 소수점에 대해 알아보며 컴퓨터에 부정확한 연산의 원인을 공부했고 그 해결책인 BigDecimal과 그 동작원리를 공부했습니다. 독자분들도 그냥 사용하고 넘기는 것이 아닌 자세히 알아보고 원리를 공부해보는건 어떨까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침.&lt;/p&gt;</description>
      <category>CS</category>
      <author>DEV_GOLF</author>
      <guid isPermaLink="true">https://golf-dev.tistory.com/84</guid>
      <comments>https://golf-dev.tistory.com/84#entry84comment</comments>
      <pubDate>Fri, 8 Sep 2023 22:12:46 +0900</pubDate>
    </item>
    <item>
      <title>유스콘 23 후기 (부제 : 좋은 개발자로 성장하기)</title>
      <link>https://golf-dev.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 유스콘을 처음으로 오프라인 참석을 해봤습니다. 유스콘은 유쾌한 스프링이라는 오픈 카카오톡 커뮤니티에서 시작된 행사로 매년 많은 주니어 분들이 지식 공유를 위해 자원하여 발표 하고 있는 행사입니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이번에 총 6개의 발표 세션을 들었습니다. 이에대한 후기를 좀 써볼가 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모두의 Server-Sent-Events&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 들었던 세션은 server sent events를 hands on으로 따라쳐보면서 실습하며 뭔지 알아가는 시간을 갖는 발표였습니다. Server sent evnets는 websocket과 polling과 같은 기술과 비교되는 실시간성 알림같은 기능에 쓰이는 기술인데요.&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;websocket과 달리 단방향 프로토콜이며 이벤트가 서버 -&amp;gt; 클라이언트 방향으로만 흐르는 단방향 통신 채널이라 polling 방식 처럼 주기적인 클라이언트 요청이 필요하지 않아서 큰 장점을 갖고 있다는 내용등을 듣고 실습해보면서 굉장히 흥미가 생겼습니다.&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;1280&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDmRwr/btssinGP1Or/6O7WsDawuZ3ZknlczO0JFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDmRwr/btssinGP1Or/6O7WsDawuZ3ZknlczO0JFk/img.png&quot; data-alt=&quot;출처 :&amp;amp;nbsp;https://velog.io/@max9106/Spring-SSE-Server-Sent-Events%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%95%8C%EB%A6%BC&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDmRwr/btssinGP1Or/6O7WsDawuZ3ZknlczO0JFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDmRwr%2FbtssinGP1Or%2F6O7WsDawuZ3ZknlczO0JFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;718&quot; height=&quot;394&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://velog.io/@max9106/Spring-SSE-Server-Sent-Events%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%95%8C%EB%A6%BC&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 더 공부해봐서 필요한 일이 있을 때 websocket과 함께 고려해보면 어떨까란 생각이 들었던 것 같습니다. 그리고 발표자도 준비를 많이 해오셔서 그런지 궁금한 질문에도 바로 해소 시켜주셨습니다. 굿 b&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;ELK 스택을 활용해 통계성 데이터 제공 API 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 ELK 스택을 도입하여 이벤트 기반 데이터들 (구매 이벤트나 클릭 이벤트 등)을 쌓고 로그를 추출하여 활용하는 API를 만들면서 겪었던 문제나 고민등을 공유하는 발표였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용을 들으면서 ELK에 대해 많이 배웠던 유튜브 강의를 공유해주셨던 점이 좋았습니다. 특히, ELK를 쓰면서 Elastic search를 사용하면서 Index라는 데이터를 모아놓은 집합의 개념이 있는데 이 때 인덱스 기반으로 타입을 구분하기 위한 mapping을 해주는 동작으로 인해 문제가 발생할 수 있다는 내용이 기억이 납니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 필자도 회사 로깅 중앙화를 위해 ELK를 고려하고 있는데 반드시 이를 고려하여 인덱스 설계를 해야할거 같다 라는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://esbook.kimjmin.net/03-cluster/3.2-index-and-shards&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://esbook.kimjmin.net/03-cluster/3.2-index-and-shards&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1693051064524&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;3.2 인덱스와 샤드 - Index &amp;amp; Shards - Elastic 가이드북&quot; data-og-description=&quot;인덱스를 생성할 때 별도의 설정을 하지 않으면 7.0 버전부터는 디폴트로 1개의 샤드로 인덱스가 구성되며 6.x 이하 버전에서는 5개로 구성됩니다. 클러스터에 노드를 추가하게 되면 샤드들이 각 &quot; data-og-host=&quot;esbook.kimjmin.net&quot; data-og-source-url=&quot;https://esbook.kimjmin.net/03-cluster/3.2-index-and-shards&quot; data-og-url=&quot;https://esbook.kimjmin.net/03-cluster/3.2-index-and-shards&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dn5cIB/hyTL9HGnuS/GVAsiVv0CP2kwzRUYjX4kk/img.png?width=1280&amp;amp;height=390&amp;amp;face=0_0_1280_390,https://scrap.kakaocdn.net/dn/DOU13/hyTMe3iWCN/V4IbNHkguWQ2NUwouyGpv0/img.png?width=1280&amp;amp;height=351&amp;amp;face=0_0_1280_351,https://scrap.kakaocdn.net/dn/mqCiA/hyTIRWchmt/i9eYQyr4zWgjkUJQI5bNY0/img.png?width=1280&amp;amp;height=351&amp;amp;face=0_0_1280_351&quot;&gt;&lt;a href=&quot;https://esbook.kimjmin.net/03-cluster/3.2-index-and-shards&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://esbook.kimjmin.net/03-cluster/3.2-index-and-shards&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dn5cIB/hyTL9HGnuS/GVAsiVv0CP2kwzRUYjX4kk/img.png?width=1280&amp;amp;height=390&amp;amp;face=0_0_1280_390,https://scrap.kakaocdn.net/dn/DOU13/hyTMe3iWCN/V4IbNHkguWQ2NUwouyGpv0/img.png?width=1280&amp;amp;height=351&amp;amp;face=0_0_1280_351,https://scrap.kakaocdn.net/dn/mqCiA/hyTIRWchmt/i9eYQyr4zWgjkUJQI5bNY0/img.png?width=1280&amp;amp;height=351&amp;amp;face=0_0_1280_351');&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;3.2 인덱스와 샤드 - Index &amp;amp; Shards - Elastic 가이드북&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;인덱스를 생성할 때 별도의 설정을 하지 않으면 7.0 버전부터는 디폴트로 1개의 샤드로 인덱스가 구성되며 6.x 이하 버전에서는 5개로 구성됩니다. 클러스터에 노드를 추가하게 되면 샤드들이 각&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;esbook.kimjmin.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사이트는 관련 공식 문서입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 그 이상의 가치 - 주니어 개발자의 사실과 오해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자를 포함한 많은 주니어 개발자들이 기술에 늪에 빠지기 쉬운데 실제로 발표자가 이런 늪에 빠져있다 빠져나오면서 느낀 자신의 생각을 공유하는 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 처음 회사 입사해서 다양한 기술적인 도전을 했던 경험이 있었고 그 과정에서 많은 실패를 겪었었는데 대부분 기술의 늪에 빠져 환경을 고려하지 못한 경우가 많았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 필요한걸까? 지금 환경에 적합한 기술일까?를 고려하는 습관을 길러보면 좋을 것 같다란 생각이 들었습니다. 또, 기술은 그저 수단이고 기술은 학습에 수단으로 봐야지 기술에 집착해선 안된다는 문구도 인상적이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼? 기술을 학습에 대상으로 봐야한다는 건 무엇일까요? 집착이랑은 다른걸까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 생각을 공유해드리자면 기술을 학습하는 이유는 뭘까요? 필자는 문제를 해결하기 위한 선택지를 늘리기 위함이라고 생각합니다. 그러니 선택지를 넓히기 위한 학습의 대상 그 이상도 이하도 아닌 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그치만 남들도 하니 우리도 해야한다. 내가 배워봤는데 이게 성능이 더 좋으니 무조건 이걸 쓰는게 좋다 라는 등의 얘기는 집착에 가깝습니다. 기술에 집착하고 기술 사용에 대한 환상을 갖고 있기 때문에 오버엔지니어링의 늪에 빠지기 쉽죠.&lt;/p&gt;
&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;h2 data-ke-size=&quot;size26&quot;&gt;주어진&amp;nbsp; 환경내에서 최대한 문제 해결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 번째 발표 내용은 주어진 환경 내에서 주어진 지식을 갖고 문제를 해결한 경험을 공유하는 내용을 들었습니다. 당시 회사에서 서버에 대한 의존성을 분리하기 위해 Message Queue를 쓰려고 했지만 러닝커브나 일정 문제로 Database와 scheduler를 이용하여 데이터를 처리하는 방식으로 해결한 경험을 공유해줬습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 저도 얼마전 docker swarm을 도입했을 때도 비슷한 경험을 했었어서 저도 공감이 많이 되는 내용이었습니다. Kubernetes의 러닝커브나 세팅 등은 많은 러닝커브를 요구했었는데요. 이러한 문제로 도입하는데 쉽진 않았습니다. 그래서 그 상황에서 최대한 문제를 해결하기 위한 가장 나은 솔루션이고 기존에 docker를 사용해봤기 때문에 제 지식에 있는 내용을 기반으로 최대한 빠르게 일을 진행했던 기억이 있었는데요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 발표 내용도 그에 대한 내용들을 다루고 있어 이입하여 들었던 것 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;너 납치 된 거야 (feat. Devops)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다섯 번째는 주니어 데브옵스분이 나오셔서 데브옵스가 무엇인지 설명하는 시간이었습니다. 아무래도 유스콘 특성상 대부분 백엔드 개발자였어서 데브옵스의 프로세스를 설명하고 개발자도 데브옵스를 왜 알아야하는지 위주로 설명을 하셨습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 회사에서 비슷한(?) 업무를 수행했다 보니 개발자도 데브옵스를 알아야한다는 생각에 많이 공감되었던 것 같습니다. 또한 데브옵스 엔지니어가 뭘 하는지도 정말 자세히 설명해주셨던거 너무 좋았던 것 같습니다. 사실 데브옵스 엔지니어가 없는 환경이라면 궁금할법한 내용이었던 것 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데브옵스를 직업이라고 오인하실 수도 있는데 데브옵스는 문제를 발견하고 해결하는 하나의 프로세스에 가깝다고 생각합니다. 그리고 발표에서도 나왔었는데 데브옵스는 철학이고 문화입니다. 문제를 해결하기 위한 모니터링 분석 개발 후 테스트 배포 등이 데브옵스 프로세스에 해당됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데브옵스 엔지니어는 이 과정에서 반복되는 업무들을 개발자가 신경쓰지 않게 지원해주는 역할을 합니다. 개발자가 비즈니스 로직에만 집중할 수 있도록 반복적인 업무들 자동화 하는데 집중합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 백엔드 개발자도 이러한 과정을 데브옵스 엔지니어가 지원해주지만 프로세스를 이해하고 있어야 고객에게 더 좋은 품질의 소프트웨어를 제공해줄 수 있는 개발 문화를 구축할 수 있다고 했던게 발표 내용에 결론이었던 것 같습니다. (내용 굿 b)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;복잡함은 끝, 간결함의 시작: 버티컬 슬라이스 아키텍처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 버티컬 슬라이스 아키텍처를 도입하여 핵사고날 아키텍처의 복잡함을 해결해나가는 과정에 대해 발표를 했었습니다. 제가 공감했던 내용은 핵사고날의 복잡함이었습니다. 물론 핵사고날은 infra에 대한 의존성을 분리하고 유연하고 확장하기 좋은 코드 아키텍처라는 장점이 있습니다만, 요구사항에 요청 값 필드가 하나만 더 늘어나도 변경해야할 코드가 많아 시간이 오래걸린 다는 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 버티컬 슬라이스라는 아키텍처를 도입하여 컨트롤러에서 모든 요청에 대한 간단한 처리를하고 반환하여 코드를 간결하게 만들어 오히려 유지보수성이 더 올라간 사례를 들었습니다. 물론 해당 아키텍처는 대규모 서비스에선 어울리지 않지만 규모가 작은 프로젝트에선 충분히 시도해봄직스러웠습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 발표를 들으면서 다양한 기술과 소프트 스킬을 배웠는데요. 정말 유익한 시간이었던 것 같습니다. 그리고 토비님과 자바지기님 만난것도 너무 좋았기도 했고 질의 응답도 많이 했었는데 정말 생각도 깊으시고 많이 배웠던것 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다양한 기술을 배운것도 좋았지만 더 좋았던건 네트워킹이었습니다. 제 블로그 구독자분도 만났었고 커뮤니티에서만 뵙던 분들을 실제로 보니 또 다른 느낌이기도 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 이런 행사가 있다면 자주가고싶네요 .... !&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침.&lt;/p&gt;</description>
      <category>개발자</category>
      <category>세미나</category>
      <category>유스콘</category>
      <category>회고</category>
      <category>후기</category>
      <author>DEV_GOLF</author>
      <guid isPermaLink="true">https://golf-dev.tistory.com/83</guid>
      <comments>https://golf-dev.tistory.com/83#entry83comment</comments>
      <pubDate>Sat, 26 Aug 2023 23:19:42 +0900</pubDate>
    </item>
  </channel>
</rss>