<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>그린 파인애플님의 블로그</title>
    <link>https://utlee0.tistory.com/</link>
    <description>파인애플님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 24 Sep 2026 04:33:05 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>그린 파인애플</managingEditor>
    <item>
      <title>[ERP 구축기 #11] Git Push에서 Kubernetes 배포까지 &amp;mdash; GitOps 파이프라인 구축기</title>
      <link>https://utlee0.tistory.com/17</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발한 풀스택 웹앱을 서비스 하려면 배포를 해야하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포와 서비스 운영을 위해서 여태까지 경험했던 springboot 내장 톰캣으로만 구동, Jeus, jenkins, k8s등 여러가지가 있었는데 그중 쿠버네티스 기반으로 운영되는 인프라에 jenkins + argoCD 를 이용하여 gitops를 구축한 경험을 풀어본다&lt;/p&gt;
&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;jeus WAS 를 구매하자니 그리 큰 서비스도 아닌데 도입하자니 비용이 비싸고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker compose 로 웹앱을 이중화하여 운영을 하는 방안도 있었으나 k8s에 비해 레퍼런스가 상대적으로 적었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평상시 쿠버네티스에 관심이 있어서 따로 공부도 해서 CKA자격증 취득도했고, 이럴때 아니면 언제 직접 해볼까 싶어 쿠버네티스 기반으로 운영하는 gitops 구축을 진행했다&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;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmQedw/dJMcaiSwy7K/jcnIbKj5Lj6rEupcam7HZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmQedw/dJMcaiSwy7K/jcnIbKj5Lj6rEupcam7HZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmQedw/dJMcaiSwy7K/jcnIbKj5Lj6rEupcam7HZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmQedw%2FdJMcaiSwy7K%2FjcnIbKj5Lj6rEupcam7HZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1952&quot; data-origin-height=&quot;1504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FFBZ2/dJMcafg9TKG/IO4KuBhD9K8kBnsDq7ETKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FFBZ2/dJMcafg9TKG/IO4KuBhD9K8kBnsDq7ETKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FFBZ2/dJMcafg9TKG/IO4KuBhD9K8kBnsDq7ETKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFFBZ2%2FdJMcafg9TKG%2FIO4KuBhD9K8kBnsDq7ETKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1952&quot; height=&quot;1504&quot; data-origin-width=&quot;1952&quot; data-origin-height=&quot;1504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 시스템이다보니 최소한의 관리포인트로 안정적인 인프라를 만들기 위해 이것저것 찾아보았고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gitOps 구축을 위해 github, jenkins, k3s, argoCD 를 이용하기로 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins만으로도 이미지 빌드부터 Kubernetes 배포까지 구성할 수 있지만, CI와 CD의 역할을 분리하고 Git에 정의된 배포 상태를 기준으로 Kubernetes 환경을 편하고, 일관되게 관리하기 위해 Argo CD를 도입했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins는 소스 빌드와 컨테이너 이미지 생성 및 GHCR Push까지 담당하고, Argo CD는 Git에 정의된 Helm 배포 구성을 기준으로 실제 Kubernetes 클러스터의 상태를 동기화하도록 구성했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github에서는 코드가 push 되면 jenkins에서 빌드를 하고, 빌드된 컨테이너 이미지는 github registry에 올려서 관리하기로 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 이미지 관리도 클라우드를 이용한다면 ECR(Elastic Container Registry) 을 이용하면 간단했지만 물리서버를 이용해야하는 환경이었기에 harbor, docker hub등을 고려했으나 github에서 무료로 제공해주는 한도내에서 충분히 쓸수 있는 github registry를 알게 되어 소스 관리와 컨테이너 이미지 관리는 github를 이용하기로 했다&lt;/p&gt;
&lt;p data-ke-size=&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;960&quot; data-origin-height=&quot;751&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmooem/dJMcai5Soaw/4SLT7hBiEfsYOPXn3mEOL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmooem/dJMcai5Soaw/4SLT7hBiEfsYOPXn3mEOL0/img.png&quot; data-alt=&quot;빌드된 이미지를 GHCR에 SHA 7자리 태그로 push를 했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmooem/dJMcai5Soaw/4SLT7hBiEfsYOPXn3mEOL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbmooem%2FdJMcai5Soaw%2F4SLT7hBiEfsYOPXn3mEOL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;751&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;751&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;빌드된 이미지를 GHCR에 SHA 7자리 태그로 push를 했다&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;kustomize, helm 중 어떤걸 사용할까 고민을 하다가 최초 구축을 하다보니 상대적으로 쉬운 kustomize를 선택 하려다가 확장성이 좋은helm을 잘 배워두면 좋을거 같아 helm 을 이용하기로 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins로 빌드가 잘되도록 pipeline을 구축했고, 플랫폼 인프라를 관리하는 repo를 별도로 만들어서 물리서버가 다운되어서 사용을 못하거나 클라우드로 이전을 고려하여 빠르게 재구축을 할수 있도록 했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvx2nG/dJMcacEP3bl/dtapoGOeqfNEgo83Fj3uZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvx2nG/dJMcacEP3bl/dtapoGOeqfNEgo83Fj3uZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvx2nG/dJMcacEP3bl/dtapoGOeqfNEgo83Fj3uZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbvx2nG%2FdJMcacEP3bl%2FdtapoGOeqfNEgo83Fj3uZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1910&quot; height=&quot;722&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포를 하면서 이미지 빌드를 할때 매번 docker hub에서 이미지를 여러가지 당겨오다보니 빌드시간이 오래걸렸고, 그만큼 네트워크 대역폭도 많이 차지했기에 개선할 방법을 찾아보았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.다운로드 받아야할 이미지를 1개의 기초 이미지로 통합하여 만들고, jdk와 node 버전을 변경해야할때만 따로 빌드해서 사용하기로 했고 만들어진 이미지와 의존성이 있는 패키지들은 캐싱 pvc를 만들어서 담아두고 빌드를 하도록 했다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.웹앱의 프론트와 백엔드 빌드시 병렬로 빌드가 되게 했으며&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #abb2bf;&quot;&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;stage('06. Build and Push Images') {
    parallel {
        stage('Backend Image') {
            when {
                anyOf {
                    expression { params.COMPONENT == 'All' }
                    expression { params.COMPONENT == 'Backend' }
                }
            }
            steps {
                container('buildkit') {
                    sh '''
                    buildctl-daemonless.sh build --frontend dockerfile.v0 \
                      --local context=cheil_be --local dockerfile=cheil_be \
                      --import-cache type=registry,ref=${BACKEND_IMAGE}:buildcache \
                      --export-cache type=registry,ref=${BACKEND_IMAGE}:buildcache,mode=max \
                      --output type=image,name=${BACKEND_IMAGE}:${IMAGE_TAG},push=${PUSH_IMAGES}
                    '''
                }
            }
        }
        stage('Frontend Image') {
            when {
                anyOf {
                    expression { params.COMPONENT == 'All' }
                    expression { params.COMPONENT == 'Frontend' }
                }
            }
            steps {
                container('buildkit') {
                    sh '''
                    buildctl-daemonless.sh build --frontend dockerfile.v0 \
                      --local context=cheil_fe --local dockerfile=cheil_fe \
                      --import-cache type=registry,ref=${FRONTEND_IMAGE}:buildcache \
                      --export-cache type=registry,ref=${FRONTEND_IMAGE}:buildcache,mode=max \
                      --output type=image,name=${FRONTEND_IMAGE}:${IMAGE_TAG},push=${PUSH_IMAGES}
                    '''
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&lt;span&gt;Pod에는 liveness/readiness probe를 적용하여 애플리케이션 상태를 지속적으로 확인하고, 비정상 상태의 컨테이너는 자동으로 재시작되도록 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한 신규 버전 배포 과정에서 정상적으로 기동되지 않는 경우 이전 버전으로 자동 Rollback 되도록 구성했다&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;이렇게 적용후 코드에 문제가 있어서 배포가 실패했을때 빠른 원복, 빌드할때는 4~5분정도 소요되던 시간을 절반가량 단축할수 있었다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpZgkY/dJMcafVNZUi/AqBXn48ziXisXVDM8VCNX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpZgkY/dJMcafVNZUi/AqBXn48ziXisXVDM8VCNX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpZgkY/dJMcafVNZUi/AqBXn48ziXisXVDM8VCNX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpZgkY%2FdJMcafVNZUi%2FAqBXn48ziXisXVDM8VCNX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;940&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;argoCD에서는 웹앱을 배포하고 관리할수 있도록 구성을 했고, 서비스 인프라에 문제가 있을때 SSH로 연결하여 명령어로 작업을 진행해도 되지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹앱 뿐만 아니라 PostgreSQL, Valkey, Grafana Stack 등의 Kubernetes 리소스도 Helm으로 관리하고 Argo CD에 등록하여, Git에 정의된 구성과 실제 클러스터 상태를 한 곳에서 확인하고 관리, 동기화할 수 있도록 구성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1912&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw5QWU/dJMcacdAZUE/8298qxZudKk1v2JDgc6I0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw5QWU/dJMcacdAZUE/8298qxZudKk1v2JDgc6I0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw5QWU/dJMcacdAZUE/8298qxZudKk1v2JDgc6I0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw5QWU%2FdJMcacdAZUE%2F8298qxZudKk1v2JDgc6I0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1912&quot; height=&quot;557&quot; data-origin-width=&quot;1912&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L8Oqs/dJMcagtywTt/rWXImI93IJbp8YEN7H3F6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L8Oqs/dJMcagtywTt/rWXImI93IJbp8YEN7H3F6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L8Oqs/dJMcagtywTt/rWXImI93IJbp8YEN7H3F6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL8Oqs%2FdJMcagtywTt%2FrWXImI93IJbp8YEN7H3F6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1898&quot; height=&quot;888&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;888&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;서비스 사용자가 많아질 경우 pod 증설도 빠르게 할수 있는것도 테스트를 진행했고, blue/green 배포, rollback도 정상적으로 되는걸 확인했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 이용을 위해서는 주기적으로 SSL인증서 갱신을 해야하기에 서버에서 터미널로 작업을 하는것보다 간편하게 할수 있도록 cert-manager를 적용하여 Kubernetes에서 인증서를 관리하도록 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 외부CA기관에서 발급받은 인증서를 사용하는 경우 자동 갱신이 어려웠기 때문에, PEM 인증서와 Private Key를 Jenkins Parameter로 전달하면 Kubernetes TLS Secret을 갱신할 수 있도록 별도의 jenkins Job을 구성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1451&quot; data-origin-height=&quot;155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buSxaC/dJMcahzffvH/KWKWkPZ9uCoTJEGhptwiek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buSxaC/dJMcahzffvH/KWKWkPZ9uCoTJEGhptwiek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buSxaC/dJMcahzffvH/KWKWkPZ9uCoTJEGhptwiek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuSxaC%2FdJMcahzffvH%2FKWKWkPZ9uCoTJEGhptwiek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1451&quot; height=&quot;155&quot; data-origin-width=&quot;1451&quot; data-origin-height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;785&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nNWeh/dJMcaiSwrhv/GequkYaNkIZbxzRzQXUxk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nNWeh/dJMcaiSwrhv/GequkYaNkIZbxzRzQXUxk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nNWeh/dJMcaiSwrhv/GequkYaNkIZbxzRzQXUxk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnNWeh%2FdJMcaiSwrhv%2FGequkYaNkIZbxzRzQXUxk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;968&quot; height=&quot;785&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;785&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;  쿠버네티스 기반 인프라와 gitops를 직접 구축해보며...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경된 코드를 Push하는 것부터 이미지 빌드, Registry 저장, Kubernetes 배포까지 직접 구성하면서 개발뿐만 아니라 실제 서비스를 운영하기 위해 필요한 인프라 구조를 경험할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 개발과 인프라를 함께 운영해야 하는 환경이다 보니 반복적으로 해야 하는 작업을 최대한 자동화하는 방향으로 구축했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub &amp;rarr; Jenkins &amp;rarr; GHCR &amp;rarr; Argo CD &amp;rarr; k3s로 이어지는 배포 Pipeline을 구성하고, Helm을 이용해 애플리케이션과 인프라 구성을 코드로 관리했다. 여기에 Grafana 모니터링과 Alertmanager 알림, 인증서 관리까지 추가하면서 작은 규모지만 개발부터 배포와 운영까지 이어지는 환경을 직접 구축할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 개선할 부분은 많지만 앞으로 실제 서비스를 운영하면서 필요한 도구들을 하나씩 추가해보고, 장애 대응과 모니터링 경험도 함께 쌓아보려고 한다.&lt;/p&gt;</description>
      <category>개발/[ERP] 풀스택 AI개발</category>
      <category>GitOps</category>
      <category>k3s</category>
      <category>k8s</category>
      <category>쿠버네티스</category>
      <author>그린 파인애플</author>
      <guid isPermaLink="true">https://utlee0.tistory.com/17</guid>
      <comments>https://utlee0.tistory.com/17#entry17comment</comments>
      <pubDate>Tue, 22 Sep 2026 22:06:43 +0900</pubDate>
    </item>
    <item>
      <title>[ERP 구축기 #10] 경력증명서 입력만 몇 시간? PDF 파싱으로 DB 입력 자동화하기</title>
      <link>https://utlee0.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현 시스템 구축전 사전 인터뷰당시 얘기가 나왔던 경력사항 자동 입력에 대해서 시스템 구현을 한걸 풀어보려 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://utlee0.tistory.com/8&quot;&gt;https://utlee0.tistory.com/8&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1789446574360&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;[ERP 구축기 #02] &amp;quot;경력사항 입력만 하다 퇴근합니다&amp;quot; - 현업 인터뷰에서 시작된 업무 자동화&quot; data-og-description=&quot;ERP 시스템을 구축하면서 가장 중요하다고 느끼는 것은 업무에 대한 이해와 데이터다.최신 기술을 잘 활용하는 것도 중요하지만, 기술은 결국 실제 업무에서 발생하는 문제를 해결하기 위한 도&quot; data-og-host=&quot;utlee0.tistory.com&quot; data-og-source-url=&quot;https://utlee0.tistory.com/8&quot; data-og-url=&quot;https://utlee0.tistory.com/8&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3A8mN/dJMb9ibbqYV/oIyuKz3KW21IQdFUdffxIk/img.png?width=491&amp;amp;height=203&amp;amp;face=0_0_491_203,https://scrap.kakaocdn.net/dn/dW53Ck/dJMb9hDlpt4/6sJu21k1h6dt3qxVBpvbQ0/img.png?width=491&amp;amp;height=203&amp;amp;face=0_0_491_203,https://scrap.kakaocdn.net/dn/f3mW2/dJMb9lMvMiq/cCXdfpxlC6mkoJIbc2xG41/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://utlee0.tistory.com/8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://utlee0.tistory.com/8&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3A8mN/dJMb9ibbqYV/oIyuKz3KW21IQdFUdffxIk/img.png?width=491&amp;amp;height=203&amp;amp;face=0_0_491_203,https://scrap.kakaocdn.net/dn/dW53Ck/dJMb9hDlpt4/6sJu21k1h6dt3qxVBpvbQ0/img.png?width=491&amp;amp;height=203&amp;amp;face=0_0_491_203,https://scrap.kakaocdn.net/dn/f3mW2/dJMb9lMvMiq/cCXdfpxlC6mkoJIbc2xG41/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[ERP 구축기 #02] &quot;경력사항 입력만 하다 퇴근합니다&quot; - 현업 인터뷰에서 시작된 업무 자동화&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ERP 시스템을 구축하면서 가장 중요하다고 느끼는 것은 업무에 대한 이해와 데이터다.최신 기술을 잘 활용하는 것도 중요하지만, 기술은 결국 실제 업무에서 발생하는 문제를 해결하기 위한 도&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;utlee0.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PQ업무를 위해서는 사전심사 자료 작성을 위해 건설기술협회 홈페이지에서 공식적으로 신고가 된 이력을 기반으로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경력증명서를 발급받아서 기술인들의 경력증명 데이터 입력을 하고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 입력해야할 사람들이 1~2명이 아니기도 하고 입력해야할 항목들이 자격사항, 학력, 기술등급, 인적사항, 상훈 , 근무처,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 이력등 많기도 하며, 경력이 쌓일수록 그만큼 경력증명서에 담긴 내용이 많기에 입력해야할 항목들도 비례해서 많아지게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람손으로 일일히 복사 붙여넣기, 검증을 하다보니 실수가 있을수 있고, 꼭 필요한 업무이지만 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;단순 반복업무이다보니&lt;span&gt; 자동화 할수 있는 방법이 없을까 싶어 개발을 하게 되었다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lp6eY/dJMcafuwNdm/nIx5qF8SKPbXUKuczVwWak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lp6eY/dJMcafuwNdm/nIx5qF8SKPbXUKuczVwWak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lp6eY/dJMcafuwNdm/nIx5qF8SKPbXUKuczVwWak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flp6eY%2FdJMcafuwNdm%2FnIx5qF8SKPbXUKuczVwWak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;987&quot; height=&quot;908&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;1283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HV7n7/dJMcaiERaqu/445oNdg69fDVsGp2DmZQJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HV7n7/dJMcaiERaqu/445oNdg69fDVsGp2DmZQJK/img.png&quot; data-alt=&quot;PDF 문서 입력 자동화 기능 구현을 위한 프로세스 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HV7n7/dJMcaiERaqu/445oNdg69fDVsGp2DmZQJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHV7n7%2FdJMcaiERaqu%2F445oNdg69fDVsGp2DmZQJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1226&quot; height=&quot;1283&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;1283&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PDF 문서 입력 자동화 기능 구현을 위한 프로세스 플로우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인사이력 정보이기에 인사정보를 관리하는 업무 화면에서 아래 사진과 같이 PDF 업로드 버튼을 클릭후 업로드하면 백엔드에서 자동 파싱하여 화면에서 파싱된 데이터를 볼수있게 하였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 초기에는 PDF 전체 텍스트를 순차적으로 추출하여 데이터를 구분하려고 했다. &lt;br /&gt;&lt;br /&gt;하지만 경력증명서는 단순한 문장 형태의 문서가 아니라. 여러&amp;nbsp;개의&amp;nbsp;표와&amp;nbsp;반복되는&amp;nbsp;프로젝트&amp;nbsp;이력으로&amp;nbsp;구성되어&amp;nbsp;있어 &lt;br /&gt;텍스트의&amp;nbsp;위치나&amp;nbsp;순서만으로&amp;nbsp;데이터를&amp;nbsp;구분하기&amp;nbsp;어려웠다. &lt;br /&gt;&lt;br /&gt;그래서 PDF 전체를 하나의 텍스트로 처리하는 방식 대신 문서의&amp;nbsp;표(Table)&amp;nbsp;영역을&amp;nbsp;기준으로&amp;nbsp;데이터를&amp;nbsp;분리하고, &lt;br /&gt;각&amp;nbsp;표별로&amp;nbsp;필요한&amp;nbsp;데이터를&amp;nbsp;구조화하는&amp;nbsp;방식으로&amp;nbsp;변경하였다. &lt;br /&gt;&lt;br /&gt;이 방식으로 변경하면서 기술인 인적사항, 프로젝트(명, 기간, 발주처) 등의 데이터를 훨씬 정확하게 추출할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eh6RYw/dJMcacR0ow6/K9PUwzpVfWIi7mgAsonDH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eh6RYw/dJMcacR0ow6/K9PUwzpVfWIi7mgAsonDH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eh6RYw/dJMcacR0ow6/K9PUwzpVfWIi7mgAsonDH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feh6RYw%2FdJMcacR0ow6%2FK9PUwzpVfWIi7mgAsonDH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1898&quot; height=&quot;868&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;868&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;891&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpF8o7/dJMcafBkGcb/wIWSm0Jriy5NR8dNGrjNC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpF8o7/dJMcafBkGcb/wIWSm0Jriy5NR8dNGrjNC0/img.png&quot; data-alt=&quot;이미 인사정보가 등록된 사람일 경우 신규 입력사항인지 정보 업데이트 인지 선택이 필요하다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpF8o7/dJMcafBkGcb/wIWSm0Jriy5NR8dNGrjNC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpF8o7%2FdJMcafBkGcb%2FwIWSm0Jriy5NR8dNGrjNC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1914&quot; height=&quot;891&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;891&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미 인사정보가 등록된 사람일 경우 신규 입력사항인지 정보 업데이트 인지 선택이 필요하다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PDF 데이터 파싱후 업무담당자가 직접 데이터 검증 후&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;1.기술인 기초정보 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.회사 실적 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.입력된 회사 실적에 프로젝트를 참가한 사람으로 입력&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1845&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GYIxX/dJMcaf2jtut/6WDaXaaqP2kvkp7gUYWEwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GYIxX/dJMcaf2jtut/6WDaXaaqP2kvkp7gUYWEwK/img.png&quot; data-alt=&quot;신규 기술인으로 선택한 경우 데이터 자체가 최초 입력이기에 추출된 데이터만 검증할수 있도록 했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GYIxX/dJMcaf2jtut/6WDaXaaqP2kvkp7gUYWEwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGYIxX%2FdJMcaf2jtut%2F6WDaXaaqP2kvkp7gUYWEwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1845&quot; height=&quot;812&quot; data-origin-width=&quot;1845&quot; data-origin-height=&quot;812&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;신규 기술인으로 선택한 경우 데이터 자체가 최초 입력이기에 추출된 데이터만 검증할수 있도록 했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1877&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JXEL7/dJMcaiSp1KE/3LClweS0PcFn7E11jbAcw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JXEL7/dJMcaiSp1KE/3LClweS0PcFn7E11jbAcw1/img.png&quot; data-alt=&quot;기술인 정보 업데이트인 경우 기존데이터를 검증하고 수정사항이 있는걸 확인/수정하여 항목마다 저장을 할수 있도록 구성했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JXEL7/dJMcaiSp1KE/3LClweS0PcFn7E11jbAcw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJXEL7%2FdJMcaiSp1KE%2F3LClweS0PcFn7E11jbAcw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1877&quot; height=&quot;790&quot; data-origin-width=&quot;1877&quot; data-origin-height=&quot;790&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기술인 정보 업데이트인 경우 기존데이터를 검증하고 수정사항이 있는걸 확인/수정하여 항목마다 저장을 할수 있도록 구성했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PDF에서 추출한 프로젝트명은 DB에 등록된 사업명과 완전히 동일하지 않고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;띄어쓰기나 일부 글자가 다른 &lt;/span&gt;경우가 많았다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;예) DB: 김해 일반산업단지 진입도로 개설공사 전면책임감리용역&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;PDF: 김해일반산업단지 진입도로 개설공사 전면 책임감리용역&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;단순 일치(=) 검색 대신 문자열내 띄어쓰기를 없애고 pdf에서 추출된 사업명과 유사도를 비교하도록 구성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정 수준(80%) 이상의 유사도를 가진 사업이 있는지, 없다면 신규 입력해야하고, 80%이상의 유사도라면 이미 입력된 회사 실적을 검증후, 최종 매핑 여부는 담당자가 직접 판단하도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;text-align: center; caret-color: transparent; letter-spacing: 0px;&quot; src=&quot;https://blog.kakaocdn.net/dna/cwapZ1/dJMcaati9At/AAAAAAAAAAAAAAAAAAAAAFsiXOF-lN3f27aGi-B7Zk92FUR5gLIZPSu_gcKuyl7W/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1790780399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=%2FuiZI9uLo6tn9t5b1pY7OGVFKAk%3D&quot; data-origin-width=&quot;1893&quot; data-origin-height=&quot;851&quot; data-is-animation=&quot;false&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpPi6s/dJMcajp2IOA/aG44K8ptwk1IIvuBLWoPZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpPi6s/dJMcajp2IOA/aG44K8ptwk1IIvuBLWoPZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpPi6s/dJMcajp2IOA/aG44K8ptwk1IIvuBLWoPZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpPi6s%2FdJMcajp2IOA%2FaG44K8ptwk1IIvuBLWoPZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1908&quot; height=&quot;826&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;826&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;h3 data-ke-size=&quot;size23&quot;&gt;자동 입력보다 중요한 것은 검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PQ 입찰에 사용되는 경력정보는 잘못 입력될 경우 실제 심사 결과에도 영향을 줄 수 있는 중요한 데이터다&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;blockquote data-ke-style=&quot;style3&quot;&gt;기존(AS-IS)&lt;br /&gt;PDF 확인 &amp;rarr; 데이터 입력 &amp;rarr; 담당자 검증 검증 &lt;br /&gt;*한글문서로 작성했기에 매번 처음부터 입력&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;개선(TO-BE)&lt;br /&gt;PDF 업로드 &amp;rarr; 자동 추출 &amp;rarr; 담당자 검증 &amp;rarr; 저장 &lt;br /&gt;* DB에 입력을 했기에 추후 이력사항 재사용 가능&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;사람이 직접 데이터를 입력하는 시간을 줄이고, 재사용성을 높였으며&lt;br /&gt;시스템이 추출한 결과가 정확한지를 확인하는 업무에 집중하도록 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞으로도 실제 업무에서 발생하는 불편함과 반복 작업을 하나씩 찾아 더 효율적인 시스템으로 개선해 나가려고 한다&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/[ERP] 풀스택 AI개발</category>
      <category>pdf입력자동화</category>
      <category>귀찮은거 싫어</category>
      <category>자동화</category>
      <author>그린 파인애플</author>
      <guid isPermaLink="true">https://utlee0.tistory.com/16</guid>
      <comments>https://utlee0.tistory.com/16#entry16comment</comments>
      <pubDate>Tue, 15 Sep 2026 14:06:11 +0900</pubDate>
    </item>
    <item>
      <title>[ERP 구축기 #09] Redis 캐싱 대체용으로 적용한 Valkey</title>
      <link>https://utlee0.tistory.com/15</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 개발시 Redis를 이용하여 공통코드, 세션관리, 정책등을 캐싱을 진행한 경험이 있는데 현재까지 잘 사용을 하고 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  인터넷을 찾아보니 클라우드 사업자들이 redis 를&amp;nbsp; 그대로 서비스하는것을 제한하려는 의도로 2024년 3월 Redis Inc.가 Redis 7.4부터 라이선스를 &lt;b&gt;RSALv2 + SSPLv1 로 변경되었다고 한다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Redis는 Redis 8부터 OSI 승인 오픈소스 라이선스인 AGPLv3를 추가했기 때문에 현재 Redis를 단순히 비오픈소스라고 표현하는 것은 정확하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Valkey는 기존 Redis OSS와 동일한 BSD 3-Clause 라이선스를 유지하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mAy9A/dJMcaix603C/PkLWz51kTBo53Pwl3336S1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mAy9A/dJMcaix603C/PkLWz51kTBo53Pwl3336S1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mAy9A/dJMcaix603C/PkLWz51kTBo53Pwl3336S1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmAy9A%2FdJMcaix603C%2FPkLWz51kTBo53Pwl3336S1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&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;Redis OSS 7.2.4를 기반으로 Valkey가 분기된 이후 Redis와 Valkey는 각각 독립적으로 발전하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 내부 시스템의 캐시 용도만 고려하면 Redis로도 충분하지만, 장기적인 오픈소스 라이선스 정책과 지원 기간, Linux Foundation 기반의 거버넌스, 지속적인 성능 개선 등을 고려하여 Valkey로 전환하기로 했다.&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;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;영역&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff; text-align: start;&quot;&gt; 기존 Redis OSS 7.2 계열 &lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Valkey 8 / 9 개선&lt;/td&gt;
&lt;td&gt;실제 효과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;멀티코어 활용&lt;/td&gt;
&lt;td&gt;메인 스레드 의존도가 큼&lt;/td&gt;
&lt;td&gt;I/O Thread와 메인 스레드 동시 처리 강화&lt;/td&gt;
&lt;td&gt;CPU 코어 활용률 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비동기 I/O&lt;/td&gt;
&lt;td&gt;I/O Thread 기능은 있으나 제한적&lt;/td&gt;
&lt;td&gt;네트워크 I/O와 일부 작업을 I/O Thread로 적극 분산&lt;/td&gt;
&lt;td&gt;처리량 증가, 지연 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command Batching&lt;/td&gt;
&lt;td&gt;상대적으로 제한적&lt;/td&gt;
&lt;td&gt;명령을 묶어 처리하고 메모리 접근 최적화&lt;/td&gt;
&lt;td&gt;높은 QPS에서 효율 향상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline 최적화&lt;/td&gt;
&lt;td&gt;기본 Pipeline&lt;/td&gt;
&lt;td&gt;Memory Prefetch 도입&lt;/td&gt;
&lt;td&gt;Valkey 9 기준 특정 테스트에서 최대 40% 처리량 향상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-copy&lt;/td&gt;
&lt;td&gt;데이터 복사 발생&lt;/td&gt;
&lt;td&gt;대용량 응답의 불필요한 복사 감소&lt;/td&gt;
&lt;td&gt;특정 워크로드에서 최대 20% 처리량 향상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS 처리&lt;/td&gt;
&lt;td&gt;메인 스레드 부담&lt;/td&gt;
&lt;td&gt;TLS handshake 등 I/O Thread로 이동&lt;/td&gt;
&lt;td&gt;TLS 연결 성능 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replica 처리&lt;/td&gt;
&lt;td&gt;복제 스트림이 부하 요인&lt;/td&gt;
&lt;td&gt;Replica의 replication stream read를 I/O Thread로 이동&lt;/td&gt;
&lt;td&gt;Replica 읽기 처리 능력 향상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster 확장성&lt;/td&gt;
&lt;td&gt;대규모 클러스터에서 관리 부담 증가&lt;/td&gt;
&lt;td&gt;대규모 Cluster 최적화&lt;/td&gt;
&lt;td&gt;Valkey 9에서 최대 2,000노드 규모 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slot Migration&lt;/td&gt;
&lt;td&gt;Key 단위 Migration&lt;/td&gt;
&lt;td&gt;Atomic Slot Migration&lt;/td&gt;
&lt;td&gt;Resharding 안정성 향상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster DB&lt;/td&gt;
&lt;td&gt;Cluster는 사실상 DB 0만 사용&lt;/td&gt;
&lt;td&gt;Valkey 9부터 Numbered DB 지원&lt;/td&gt;
&lt;td&gt;Cluster에서도 논리적 DB 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라이선스/거버넌스&lt;/td&gt;
&lt;td&gt;Redis Inc. 중심&lt;/td&gt;
&lt;td&gt;BSD 3-Clause + Linux Foundation&lt;/td&gt;
&lt;td&gt;벤더 중립적인 OSS 운영&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-end=&quot;2073&quot; data-start=&quot;1943&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2073&quot; data-start=&quot;1943&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-size: 1.25em; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;  ERP/PQ 시스템에서는 Redis의 GET, SET, DELETE, TTL 등 기본적인 캐시 명령을 주로 사용하고 있었고, 기존 Java 코드도 대부분 호환되어 큰 부담 없이 Valkey로 전환할 수 있었다&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2073&quot; data-start=&quot;1943&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2073&quot; data-start=&quot;1943&quot; data-ke-size=&quot;size16&quot;&gt;현재 구축 중인 ERP/PQ 시스템은 수십 명 수준의 사용자를 대상으로 하기 때문에 Valkey의 대규모 Cluster 성능이나 초고성능 처리량이 필요한 환경은 아니다. 따라서 이번 전환에서 성능 자체가 가장 중요한 이유는 아니었다.&lt;/p&gt;
&lt;p data-end=&quot;2073&quot; data-start=&quot;1943&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2245&quot; data-start=&quot;2078&quot; data-ke-size=&quot;size16&quot;&gt;Redis OSS와 높은 호환성을 유지하면서 BSD 3-Clause 라이선스를 사용하고 있고, Linux Foundation을 중심으로 지속적인 개발이 이루어지고 있다는 점을 더 중요하게 판단했다. 또한 향후 시스템 규모가 커지더라도 Cluster 등의 확장 기능을 활용할 수 있다는 점도 고려했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서는 Spring Data Redis를 그대로 유지했다, 컨테이너 이미지와 연결 설정을 Valkey 환경으로 변경하는 정도로 전환할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현시점 Valkey 최신버전인 v9.1.2 로 적용하여 테스트시 문제 없이 잘 작동하는걸 확인했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Redis 데이터는 별도로 마이그레이션하지 않았다. 원본 데이터는 PostgreSQL에 존재하고 Redis는 Cache Aside 방식의 캐시 저장소로 사용하고 있기 때문에, Valkey를 빈 상태로 실행한 뒤 Cache Miss 발생 시 데이터를 다시 캐싱하도록 했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;739&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn5KHs/dJMcaath5nI/q80L9K8GhF3WO636AdcLB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn5KHs/dJMcaath5nI/q80L9K8GhF3WO636AdcLB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn5KHs/dJMcaath5nI/q80L9K8GhF3WO636AdcLB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn5KHs%2FdJMcaath5nI%2Fq80L9K8GhF3WO636AdcLB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1254&quot; height=&quot;739&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;739&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;grafana 에서 캐시 조회, hit 율도 이상없이 잘 동작함을 확인했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1859&quot; data-origin-height=&quot;653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caLmnO/dJMcacEK0zE/EyOjrvstkSFleiEL7Lxfwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caLmnO/dJMcacEK0zE/EyOjrvstkSFleiEL7Lxfwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caLmnO/dJMcacEK0zE/EyOjrvstkSFleiEL7Lxfwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaLmnO%2FdJMcacEK0zE%2FEyOjrvstkSFleiEL7Lxfwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1859&quot; height=&quot;653&quot; data-origin-width=&quot;1859&quot; data-origin-height=&quot;653&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;3908&quot; data-start=&quot;3752&quot; data-ke-size=&quot;size23&quot;&gt;  Valkey가 모든 면에서 Redis보다 우수한 것은 아니다&lt;/h3&gt;
&lt;p data-end=&quot;3908&quot; data-start=&quot;3752&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 오랫동안 사용된 만큼 관련 문서와 클라이언트, 운영 도구 등 생태계가 더 크다. 또한 Redis와 Valkey가 각각 독립적으로 개발되고 있기 때문에 향후 두 제품의 기능 차이가 점차 커질 가능성도 있다.&lt;/p&gt;
&lt;p data-end=&quot;3996&quot; data-start=&quot;3913&quot; data-ke-size=&quot;size16&quot;&gt;따라서 Valkey 고유 기능에 의존하기보다는 GET/SET/DEL/TTL 등 Redis와 공통으로 사용할 수 있는 기능을 중심으로 사용하기로 했다.&lt;/p&gt;
&lt;p data-end=&quot;3996&quot; data-start=&quot;3913&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3996&quot; data-start=&quot;3913&quot; data-ke-size=&quot;size16&quot;&gt;이번 전환에서 느낀 점은 Valkey가 단순히 Redis를 대체하기 위해 만들어진 Fork에 머무르지 않고 독립적인 프로젝트로 빠르게 발전하고 있다는 점이었다. 현재 시스템 규모에서는 Redis와 Valkey의 성능 차이를 체감하기 어렵지만, 기존 Spring Data Redis구조를 거의 변경하지 않고 전환할 수 있었고 장기적인 프로젝트 방향까지 고려했을 때 신규 시스템에서는 Valkey를 사용하는 것이 더 적합하다고 판단했다&lt;/p&gt;</description>
      <category>개발/[ERP] 풀스택 AI개발</category>
      <category>Redis</category>
      <category>valkey</category>
      <category>캐시</category>
      <author>그린 파인애플</author>
      <guid isPermaLink="true">https://utlee0.tistory.com/15</guid>
      <comments>https://utlee0.tistory.com/15#entry15comment</comments>
      <pubDate>Mon, 14 Sep 2026 10:41:40 +0900</pubDate>
    </item>
    <item>
      <title>[ERP 구축기 #08] 엑셀과 전화로 관리하던 기술인 교육관리를 알림 발송으로 자동화하기</title>
      <link>https://utlee0.tistory.com/14</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 수백 명의 교육일정을 Excel로 관리하고 있었다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무 인터뷰를 진행하면서 생각보다 큰 개선 포인트 하나를 발견했다.&lt;/p&gt;
&lt;p data-end=&quot;1965&quot; data-start=&quot;1873&quot; data-ke-size=&quot;size16&quot;&gt;건설업에서는 기술인들이 일정 주기마다 필요한 교육을 이수해야 한다. 3년 주기의 교육부터 기술사 교육, 진급에 필요한 교육 등 관리해야 하는 교육의 종류도 다양했다.&lt;/p&gt;
&lt;p data-end=&quot;2033&quot; data-start=&quot;1970&quot; data-ke-size=&quot;size16&quot;&gt;문제는 &lt;b&gt;수백 명에 이르는 기술인의 교육 이수 여부와 교육 예정일을 Excel로 관리하고 있었다는 점&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-end=&quot;2087&quot; data-start=&quot;2038&quot; data-ke-size=&quot;size16&quot;&gt;교육 기한이 다가오면 담당자가 대상자를 확인하고 메신저나 전화로 일일이 안내하고 있었다.&lt;/p&gt;
&lt;blockquote data-end=&quot;2128&quot; data-start=&quot;2092&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;2128&quot; data-start=&quot;2094&quot; data-ke-size=&quot;size16&quot;&gt;&quot;OOO 교육을 OO 홈페이지에서 신청해서 XX까지 이수하셔야 합니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;2226&quot; data-start=&quot;2133&quot; data-ke-size=&quot;size16&quot;&gt;업계 특성상 연령대가 높기에 한 번 안내한다고 끝나는 것도 아니었다. 신청 방법을 다시 물어보거나 대신 신청해 달라는 요청, 알겠다고만 하고 실제로 신청을 늦게하여 기한이 초과되는등의 사례도 있어 담당자는 기존 업무를 처리하면서 같은 안내를 계속 반복해야 했다.&lt;/p&gt;
&lt;p data-end=&quot;2282&quot; data-start=&quot;2231&quot; data-ke-size=&quot;size16&quot;&gt;더 큰 문제는 필요한 교육을 이수하지 않으면 프로젝트 참여에 영향을 줄 수 있다는 점이었다.&lt;/p&gt;
&lt;p data-end=&quot;2381&quot; data-start=&quot;2287&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2381&quot; data-start=&quot;2287&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 ERP 구축에서는 &lt;b&gt;교육 대상자를 시스템에서 관리하고 교육 시점이 다가오면 담당자가 쉽게 확인하여 대상자에게 알림을 발송할 수 있도록 개선하기로 했다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2026년 9월 5일 오후 03_28_32.png&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;1374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yDhSa/dJMcaazVyje/8YSuIxK8tRXyLK0kKIHcZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yDhSa/dJMcaazVyje/8YSuIxK8tRXyLK0kKIHcZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yDhSa/dJMcaazVyje/8YSuIxK8tRXyLK0kKIHcZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyDhSa%2FdJMcaazVyje%2F8YSuIxK8tRXyLK0kKIHcZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1145&quot; height=&quot;1374&quot; data-filename=&quot;ChatGPT Image 2026년 9월 5일 오후 03_28_32.png&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;1374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  교육 대상자를 놓치지 않도록 관리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교육 대상자와 이수 기한을 시스템에서 관리하고, 교육 예정일이 가까워진 기술인은 Grid에서 바로 확인할 수 있도록 하이라이팅했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기엔 배치로 1주마다 자동 확인하여 대상자에게 알림을 발송하려고 했으나, 현재 만들고 있는 시스템은 기존 ERP에서 분리된 시스템이기도 하고, PQ업무 담당자가 인사담당자가 아니기에 현재 수백명이나 되는 기술인의 인사정보의 퇴사 데이터를 정확하게 관리하는데 한계가 있었기에 오발송되는 사례를 없애기위해 자동발송 기능은 추후에 고려하기로 했으며&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차적으로는 LMS, 카카오 알림톡 발송은 완전 자동화하지 않고 &lt;b&gt;담당자가 대상자와 내용을 확인한 뒤 발송하는 방식&lt;/b&gt;으로 구성했다.&lt;/p&gt;
&lt;p data-end=&quot;2979&quot; data-start=&quot;2895&quot; data-ke-size=&quot;size16&quot;&gt;반복적으로 사용하는 안내 문구는 템플릿으로 관리하고, 필요한 경우 담당자가 직접 내용을 작성하여 LMS 또는 카카오 알림톡으로 발송할 수 있도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&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-end=&quot;3481&quot; data-start=&quot;3401&quot; data-ke-size=&quot;size16&quot;&gt;실제 운영 전에는 전화번호의 수집&amp;middot;이용 목적과 보유 근거, 알림 발송을 위한 이용 범위 등을 회사의 개인정보 처리 기준에 따라 검토할 예정이다.&lt;/p&gt;
&lt;p data-end=&quot;4403&quot; data-start=&quot;4336&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcZdz5/dJMcaiknBWV/icUNRBpvWQyYmQURlqCksk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcZdz5/dJMcaiknBWV/icUNRBpvWQyYmQURlqCksk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcZdz5/dJMcaiknBWV/icUNRBpvWQyYmQURlqCksk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcZdz5%2FdJMcaiknBWV%2FicUNRBpvWQyYmQURlqCksk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  중요 개인정보는 암호화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전화번호는 개인정보 보호를 위해 암호화 키는 백엔드 yaml에 작성, DB와 분리하여 관리하도록 했고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DB 저장 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;AES256-GCM 방식으로 암호화&lt;/b&gt;하도록 구성했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4336&quot; data-end=&quot;4403&quot; data-ke-size=&quot;size16&quot;&gt;GCM은 암호화와 함께 데이터의 무결성을 검증할 수 있는 인증 기능을 제공하기 때문에 개인정보 암호화 방식으로 선택했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4336&quot; data-end=&quot;4403&quot; data-ke-size=&quot;size16&quot;&gt;*쿠버네티스에서 secret 키값을 주입받아 사용할수 있으므로 차후에 시도해보려고 한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;579&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjfFp4/dJMcacdlXVC/E3pU4e0iXcrSI16oVdywrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjfFp4/dJMcacdlXVC/E3pU4e0iXcrSI16oVdywrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjfFp4/dJMcacdlXVC/E3pU4e0iXcrSI16oVdywrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjfFp4%2FdJMcacdlXVC%2FE3pU4e0iXcrSI16oVdywrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;579&quot; height=&quot;311&quot; data-origin-width=&quot;579&quot; data-origin-height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2Y7G9/dJMcadwrBSn/lDEPRamunIzhJ4B6q1sctk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2Y7G9/dJMcadwrBSn/lDEPRamunIzhJ4B6q1sctk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2Y7G9/dJMcadwrBSn/lDEPRamunIzhJ4B6q1sctk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2Y7G9%2FdJMcadwrBSn%2FlDEPRamunIzhJ4B6q1sctk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mBJqL/dJMcahTaSOP/mCpz7TEILoexT1an4PygwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mBJqL/dJMcahTaSOP/mCpz7TEILoexT1an4PygwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mBJqL/dJMcahTaSOP/mCpz7TEILoexT1an4PygwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmBJqL%2FdJMcahTaSOP%2FmCpz7TEILoexT1an4PygwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAm7tD/dJMcacj6fMt/wuJfcmhb3K2TCRSuGJOZd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAm7tD/dJMcacj6fMt/wuJfcmhb3K2TCRSuGJOZd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAm7tD/dJMcacj6fMt/wuJfcmhb3K2TCRSuGJOZd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAm7tD%2FdJMcacj6fMt%2FwuJfcmhb3K2TCRSuGJOZd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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-end=&quot;4955&quot; data-start=&quot;4886&quot; data-ke-size=&quot;size16&quot;&gt;잘못된 전화번호나 외부 메시징 서비스의 오류 등으로 발송이 실패할 수 있기 때문에 발송 요청 결과를 별도로 저장하도록 했다.&lt;/p&gt;
&lt;p data-end=&quot;5043&quot; data-start=&quot;4960&quot; data-ke-size=&quot;size16&quot;&gt;담당자는 발송 이력 화면에서 &lt;b&gt;발송 성공/실패 여부와 실패 사유를 확인&lt;/b&gt;할 수 있으며, 실패한 대상자만 선택하여 다시 발송할 수 있도록 구성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;963&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mXPKu/dJMcaiq5iTw/6BOWLAsWn405sAT7wmiLWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mXPKu/dJMcaiq5iTw/6BOWLAsWn405sAT7wmiLWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mXPKu/dJMcaiq5iTw/6BOWLAsWn405sAT7wmiLWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmXPKu%2FdJMcaiq5iTw%2F6BOWLAsWn405sAT7wmiLWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1094&quot; height=&quot;963&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;963&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;h3 data-ke-size=&quot;size23&quot;&gt;  개발하면서 느낀 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무 인터뷰를 처음 진행했을 때 교육 이수관리는 불편하지만 엑셀로도 진행이 가능은 하기에 중요한 기능으로 언급되지는 않았다.&lt;/p&gt;
&lt;p data-end=&quot;6048&quot; data-start=&quot;5953&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제 업무를 자세히 들어보니 Excel에서 대상자를 찾고, 교육 기한을 확인하고, 전화나 메신저로 같은 내용을 반복해서 안내하는 데 상당한 시간이 사용되고 있었다.&lt;/p&gt;
&lt;p data-end=&quot;6048&quot; data-start=&quot;5953&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6168&quot; data-start=&quot;6053&quot; data-ke-size=&quot;size16&quot;&gt;인사정보 데이터가 실시간으로 정확하게 보장되지 않는 상태에서 시스템이 모든 과정을 대신하도록 완전히 자동화하기보다는 &lt;b&gt;교육 대상자를 자동으로 찾아주고, 담당자가 확인한 뒤 쉽게 알림을 보낼 수 있도록 만드는 것만으로도 반복 업무를 크게 줄일 수 있다고 판단했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;6276&quot; data-start=&quot;6173&quot; data-ke-size=&quot;size16&quot;&gt;이번 기능을 개발하면서 ERP의 업무 개선은 거창한 기능을 추가하는 것보다 &lt;b&gt;&amp;ldquo;귀찮지만 누군가는 계속 관리하는 일&amp;rdquo;을 찾아 줄여주는 것에서 시작할 수도 있다는 점&lt;/b&gt;을 다시 느꼈다.&lt;/p&gt;</description>
      <category>개발/[ERP] 풀스택 AI개발</category>
      <category>교육이수관리</category>
      <category>귀찮은거 싫어</category>
      <category>자동화</category>
      <author>그린 파인애플</author>
      <guid isPermaLink="true">https://utlee0.tistory.com/14</guid>
      <comments>https://utlee0.tistory.com/14#entry14comment</comments>
      <pubDate>Fri, 4 Sep 2026 17:47:31 +0900</pubDate>
    </item>
    <item>
      <title>[ERP 구축기 #07] 양식이 제각각인 HWPX 문서를 자동 생성해 비용 절감하기</title>
      <link>https://utlee0.tistory.com/13</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 만들고 있는 시스템에서 가장 공을 많이 들인 페이지 3개가 있는데 그중 2개를 소개 해보고자 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째는 PQ서류를 제출하기 위해 기술인들의 자격심사를 검증하고, 문서를 자동으로 만들어주는 업무 화면이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PQ 업무에서는 입찰에 참여할 기술인을 선정한 뒤, 해당 기술인의 자격과 프로젝트 수행 이력을 검증하여 발주처에서 요구하는 양식의 문서를 작성해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1119&quot; data-start=&quot;1056&quot; data-ke-size=&quot;size16&quot;&gt;문제는 담당자가 한 번의 입찰을 준비하기 위해 수십~수백 명의 기술인과 프로젝트 이력을 확인해야 한다는 점이었다.&lt;/p&gt;
&lt;p data-end=&quot;1217&quot; data-start=&quot;1124&quot; data-ke-size=&quot;size16&quot;&gt;기존 시스템의 조회 기능만으로는 데이터 관리의 정확성 부족과 필요한 정보를 한눈에 확인하기 어려웠고, 업무에 익숙한 담당자들은 경험과 기억에 의존해 대상 기술인을 선정하고 문서를 작성하고 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1299&quot; data-start=&quot;1222&quot; data-ke-size=&quot;size16&quot;&gt;이번 시스템을 구축하면서 이 과정을 &lt;b&gt;① 입찰 기준에&lt;span&gt; 해당하는&lt;/span&gt;&amp;nbsp;기술인 선정 및 검증 &amp;rarr; ② 선정된 기술인 기준으로 입찰 기준에 맞는 프로젝트 단위로 필터 및 검증 &lt;/b&gt;&lt;b&gt;&lt;b&gt;&amp;rarr;&lt;/b&gt; ③ 제출 문서 자동 생성&lt;/b&gt;의 3단계로 나누어 개선하기로 했다.&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;  1단계. 수백 명의 기술인 중 입찰 조건에 맞는 사람 찾기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회된 기술인을 선택하면 &lt;b&gt;입찰조건에 해당하는 프로젝트 이력, 전체 프로젝트 이력, 현재 업무중복도&lt;/b&gt;를 한 화면에서 확인할 수 있도록 구성했다.&lt;/p&gt;
&lt;p data-end=&quot;1811&quot; data-start=&quot;1762&quot; data-ke-size=&quot;size16&quot;&gt;기존처럼 여러 업무 화면을 이동하면서 정보를 확인하지 않아도 되도록 하는 것이 목표였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcNwzr/dJMcaafCeAR/Wq8UrjGQQ1IrKoXmx0tDl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcNwzr/dJMcaafCeAR/Wq8UrjGQQ1IrKoXmx0tDl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcNwzr/dJMcaafCeAR/Wq8UrjGQQ1IrKoXmx0tDl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcNwzr%2FdJMcaafCeAR%2FWq8UrjGQQ1IrKoXmx0tDl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C1Q3C/dJMcaf8Ttg2/nkvFKoXeqzzbgBglO0d1Lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C1Q3C/dJMcaf8Ttg2/nkvFKoXeqzzbgBglO0d1Lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C1Q3C/dJMcaf8Ttg2/nkvFKoXeqzzbgBglO0d1Lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC1Q3C%2FdJMcaf8Ttg2%2FnkvFKoXeqzzbgBglO0d1Lk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsTvjO/dJMcablj0mH/EhhcVIQvY24kdqRNc8JYz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsTvjO/dJMcablj0mH/EhhcVIQvY24kdqRNc8JYz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsTvjO/dJMcablj0mH/EhhcVIQvY24kdqRNc8JYz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsTvjO%2FdJMcablj0mH%2FEhhcVIQvY24kdqRNc8JYz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  2단계. 선정된 기술인의 프로젝트 이력 검증하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차적으로 자격요건에 해당되는 기술인을 필터했고, 2차적으로 기술인들이 진행했던 프로젝트들이 많기에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입찰자격에 해당하는 프로젝트만 추출할수 있도록 &quot; 기술인 실적 문서 생성 &quot; 업무 화면을 만들었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당화면에서 상세 조건으로 프로젝트만 필터할수 있도록 기능을 추가했고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 이력을 검증후 최종적으로 공공기관에서 주로 쓰는 한글문서(hwpx) 파일을 자동으로 만들수 있도록 기능을 만들었다&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;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rpaNf/dJMcahyZWkt/u6YIKRAfYafygfmurokQzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rpaNf/dJMcahyZWkt/u6YIKRAfYafygfmurokQzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rpaNf/dJMcahyZWkt/u6YIKRAfYafygfmurokQzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrpaNf%2FdJMcahyZWkt%2Fu6YIKRAfYafygfmurokQzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&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;주로 사용하는 정형화된 양식에 데이터를 맵핑해서 산출물을 만들어주는 경우라면 리포트 툴(oz report, report designer등)을 도입하여 리포팅 솔루션을 이용하면 해결될 문제였으나,&lt;/p&gt;
&lt;p data-end=&quot;2790&quot; data-start=&quot;2683&quot; data-ke-size=&quot;size16&quot;&gt;PQ 업무에서는 발주처와 공고마다 제출을 요구하는 한글 양식이 달라질 수 있었고, 양식이 변경될 때마다 개발자가 리포트 양식을 수정하는 방식은 유지보수 측면에서 적합하지 않다고 판단했다.&lt;/p&gt;
&lt;p data-end=&quot;2790&quot; data-start=&quot;2683&quot; 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-end=&quot;3150&quot; data-start=&quot;3072&quot; data-ke-size=&quot;size16&quot;&gt;웹 환경에서 한글 편집 기능을 제공하거나 Backend에서 한글 문서를 생성할 수 있는 여러 제품을 검토했고, 업체 미팅과 데모도 진행했다.&lt;/p&gt;
&lt;p data-end=&quot;3249&quot; data-start=&quot;3155&quot; data-ke-size=&quot;size16&quot;&gt;기능적으로는 요구사항을 충족할 수 있었지만 &lt;b&gt;라이선스 및 서버 구축 비용과 현재 시스템에서 실제로 필요한 기능의 범위&lt;/b&gt;를 비교했을 때 과한 구성이라고 판단했다.&lt;/p&gt;
&lt;p data-end=&quot;3303&quot; data-start=&quot;3254&quot; data-ke-size=&quot;size16&quot;&gt;그래서 생각하고 있던 HWPX 문서의 필드이름 매핑 기능을 직접 활용해보기로 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cr33XD/dJMcagGWXoC/nAGS6C0WLsKxTPK249y3Uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cr33XD/dJMcagGWXoC/nAGS6C0WLsKxTPK249y3Uk/img.png&quot; data-alt=&quot;데모로 사용해본 한글 웹기안기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cr33XD/dJMcagGWXoC/nAGS6C0WLsKxTPK249y3Uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcr33XD%2FdJMcagGWXoC%2FnAGS6C0WLsKxTPK249y3Uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1032&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데모로 사용해본 한글 웹기안기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;한글내 표에 필드 이름을 맵핑하는 기능이 있었고, 이부분을 활용하기로 했다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1189&quot; data-origin-height=&quot;949&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yvbLC/dJMcaaT8Oyt/QYK95IfMwEjPSdpsDpSl9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yvbLC/dJMcaaT8Oyt/QYK95IfMwEjPSdpsDpSl9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yvbLC/dJMcaaT8Oyt/QYK95IfMwEjPSdpsDpSl9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyvbLC%2FdJMcaaT8Oyt%2FQYK95IfMwEjPSdpsDpSl9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1189&quot; height=&quot;949&quot; data-origin-width=&quot;1189&quot; data-origin-height=&quot;949&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;span style=&quot;color: #333333; text-align: start;&quot;&gt; 해당부분에 필드이름 값을 맵핑하면 한글문서(hwpx)를 zip 파일로 강제 변경후 압축을 풀면 section0.xml 파일이 있는데&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일에 맵핑했던 필드명이 있는걸 확인했다, 인터넷을 찾아보니 필드이름 맵핑 기능을 활용해서 메일을 발송하는등 기능을 활용했던 사례가 보였고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;미리 정의한 필드가 포함된 HWPX 템플릿을 업로드하고, Backend에서 해당 필드에 DB 데이터를 매핑한 뒤 새로운 HWPX 문서를 생성하는 방식&lt;/b&gt;으로 구현할 수 있겠다고 생각했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;906&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpQtW2/dJMcadcaD6S/DAmkgTmWdIcPBZk6sGmqTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpQtW2/dJMcadcaD6S/DAmkgTmWdIcPBZk6sGmqTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpQtW2/dJMcadcaD6S/DAmkgTmWdIcPBZk6sGmqTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpQtW2%2FdJMcadcaD6S%2FDAmkgTmWdIcPBZk6sGmqTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;931&quot; height=&quot;906&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;1330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hfula/dJMcabS3qX1/53PpWNabCKlV1YuM89UvJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hfula/dJMcabS3qX1/53PpWNabCKlV1YuM89UvJ1/img.png&quot; data-alt=&quot;생각한 기능 구현 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hfula/dJMcabS3qX1/53PpWNabCKlV1YuM89UvJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHfula%2FdJMcabS3qX1%2F53PpWNabCKlV1YuM89UvJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1183&quot; height=&quot;1330&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;1330&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;h3 data-ke-size=&quot;size23&quot;&gt; ️ HWPX 한글문서내 필드이름 맵핑 방식으로 문서 자동 생성하기&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;주로 많이 쓰는 필드 맵핑값들을 취합후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵핑이 가능한 필드이름을 grid 형식으로 정리를 했고, 복사 붙여넣기도 편리하게 할수 있도록 클릭 한번으로 자동 복사 기능도 추가하였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 입장에서는 engineerName, projectName, startDate처럼 영문 필드명을 사용하는 것이 익숙하다.&lt;/p&gt;
&lt;p data-end=&quot;4401&quot; data-start=&quot;4359&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제로 한글 산출문서를 만드는 사람은 개발자가 아니라 업무 담당자다.&lt;/p&gt;
&lt;p data-end=&quot;4492&quot; data-start=&quot;4406&quot; data-ke-size=&quot;size16&quot;&gt;그래서 기본_기술인명, 이력_프로젝트명, 이력_참여시작일 처럼 &lt;b&gt;업무 담당자가 의미를 바로 이해할 수 있는 한글 필드명을 사용&lt;/b&gt;할 수 있도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날짜 포맷이 워낙 다양하기에 각 케이스를 취합받아 개발하는데 애를 먹은거 같다 &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 UI처럼 매번 달라지는 양식지에 필드값 맵핑후, 필드이름값 맵핑이 완료된 양식지를 업로드 하면 자동으로 DB에서 가져온 값을 맵핑하여 한글 산출물을 만들어 낼수 있도록 개발을 했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJoNdm/dJMcajct8UE/esHuLLN5PkUY5YxSInqUW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJoNdm/dJMcajct8UE/esHuLLN5PkUY5YxSInqUW1/img.png&quot; data-alt=&quot;참여할 공고문의 PQ 제출 문서중 기술인별 이력, 실적등 문서 자동 생성 업무 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJoNdm/dJMcajct8UE/esHuLLN5PkUY5YxSInqUW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJoNdm%2FdJMcajct8UE%2FesHuLLN5PkUY5YxSInqUW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1892&quot; height=&quot;879&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;참여할 공고문의 PQ 제출 문서중 기술인별 이력, 실적등 문서 자동 생성 업무 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;1차에서 필터한 대상 기술인들에 대해 양식지에 필드이름을 맵핑하여 한글 문서 생성이 잘된것을 확인했고 문서를 열었을때도 잘 열리는걸 확인했다&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;593&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpCAqg/dJMcafH10Pu/g0s5aaINZYK9zrTgEGboTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpCAqg/dJMcafH10Pu/g0s5aaINZYK9zrTgEGboTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpCAqg/dJMcafH10Pu/g0s5aaINZYK9zrTgEGboTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpCAqg%2FdJMcafH10Pu%2Fg0s5aaINZYK9zrTgEGboTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;806&quot; height=&quot;593&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;593&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;873&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMwqkZ/dJMcac5ArVp/Q9jAk0ekihf3ftiMDXeLvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMwqkZ/dJMcac5ArVp/Q9jAk0ekihf3ftiMDXeLvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMwqkZ/dJMcac5ArVp/Q9jAk0ekihf3ftiMDXeLvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMwqkZ%2FdJMcac5ArVp%2FQ9jAk0ekihf3ftiMDXeLvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1764&quot; height=&quot;873&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;873&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1619&quot; data-origin-height=&quot;908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NCMlv/dJMcajpZl3P/Lsc4dRi1z95XYE2DeoFXQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NCMlv/dJMcajpZl3P/Lsc4dRi1z95XYE2DeoFXQ0/img.png&quot; data-alt=&quot;출력할 업무중복도 문서 자동화 / PQ기준에 맞는 실적만 추출하여 문서 양식을 업로드 후 문서를 자동으로 만들어지게 했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NCMlv/dJMcajpZl3P/Lsc4dRi1z95XYE2DeoFXQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNCMlv%2FdJMcajpZl3P%2FLsc4dRi1z95XYE2DeoFXQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1619&quot; height=&quot;908&quot; data-origin-width=&quot;1619&quot; data-origin-height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출력할 업무중복도 문서 자동화 / PQ기준에 맞는 실적만 추출하여 문서 양식을 업로드 후 문서를 자동으로 만들어지게 했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;829&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K5oSs/dJMcahZ4whk/8BPZY1Zpcsl3yLVXQUmkqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K5oSs/dJMcahZ4whk/8BPZY1Zpcsl3yLVXQUmkqK/img.png&quot; data-alt=&quot;업무 중복도 문서 자동화 / 총 계 부분은 데이터 검증후 수기 입력하는게 낫다는 협의에 이르러서 비워둔 상태다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K5oSs/dJMcahZ4whk/8BPZY1Zpcsl3yLVXQUmkqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK5oSs%2FdJMcahZ4whk%2F8BPZY1Zpcsl3yLVXQUmkqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1033&quot; height=&quot;829&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;829&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;업무 중복도 문서 자동화 / 총 계 부분은 데이터 검증후 수기 입력하는게 낫다는 협의에 이르러서 비워둔 상태다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZBUHF/dJMb99VjvYv/X6hKVtXJM6LGmkrx1szyOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZBUHF/dJMb99VjvYv/X6hKVtXJM6LGmkrx1szyOk/img.png&quot; data-alt=&quot;회사실적 문서자동화 / PQ기준에 맞는 실적만 추출하여 문서 양식을 업로드 후 문서를 자동으로 만들어지게 했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZBUHF/dJMb99VjvYv/X6hKVtXJM6LGmkrx1szyOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZBUHF%2FdJMb99VjvYv%2FX6hKVtXJM6LGmkrx1szyOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1620&quot; height=&quot;894&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;894&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회사실적 문서자동화 / PQ기준에 맞는 실적만 추출하여 문서 양식을 업로드 후 문서를 자동으로 만들어지게 했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbwWCw/dJMb9919o5U/VLDwVGN1PsK44NsNDr1gjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbwWCw/dJMb9919o5U/VLDwVGN1PsK44NsNDr1gjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbwWCw/dJMb9919o5U/VLDwVGN1PsK44NsNDr1gjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbwWCw%2FdJMb9919o5U%2FVLDwVGN1PsK44NsNDr1gjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1642&quot; height=&quot;818&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 구현후 잘되는것을 확인해서 실무 담당자와 검토하였고, 각종 문서들을 자동화 할수 있도록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 적극적으로 PQ작성 문서 자동화를 진행했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PQ작성중 수기작업이 많이 필요한&amp;nbsp; &quot;회사실적 문서 생성&quot;, &quot;기술인 실적문서 생성&quot;, &quot;업무 중복도&quot;에 적용했고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 실적증명서, 참여 증명서 문서도 자동 생성이 가능하도록 기능 구현을 하여 업무를 하는데 편리함을 개선했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 개발하려고 하면 솔루션을 도입할수 밖에 없었을텐데 다른 시각으로 접근하여 AI덕분에 빠르고 간편하게 개발을 진행했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 시스템 내부에서 기능을 구현했고, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;4~5천만원의 예상비용이었던&amp;nbsp; 한글 문서 생성 솔루션과 서버 비용을 아낄수&lt;/span&gt;&amp;nbsp;있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  기능 구현을 하면서 느낀 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 한글 문서를 자동으로 생성해야 한다는 요구사항 때문에 상용 솔루션 도입이 가장 현실적인 방법이라고 생각했다.&lt;/p&gt;
&lt;p data-end=&quot;5503&quot; data-start=&quot;5371&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제 업무에서 필요한 기능을 다시 정리해보니 복잡한 한글 편집 기능 전체가 필요한 것은 아니었다. &lt;b&gt;업무 담당자가 기존 한글 양식에 필요한 필드를 지정하고, 시스템이 DB의 데이터를 해당 위치에 채워주는 기능&lt;/b&gt;이면 충분했다.&lt;/p&gt;
&lt;p data-end=&quot;5594&quot; data-start=&quot;5508&quot; data-ke-size=&quot;size16&quot;&gt;이를 기준으로 HWPX 파일 구조를 직접 확인하고 AI를 활용해 구현 가능성을 빠르게 검증하면서 별도의 상용 솔루션 없이 필요한 기능을 구현할 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;5691&quot; data-start=&quot;5599&quot; data-ke-size=&quot;size16&quot;&gt;이번 기능을 개발하면서 느낀 점은 &lt;b&gt;기존 솔루션을 그대로 도입하기 전에 실제 업무에서 필요한 기능이 무엇인지 범위를 정확하게 정의하는 것이 중요하다는 것&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-end=&quot;5758&quot; data-start=&quot;5696&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로 현업이 필요했던 문서 작성 자동화와 솔루션 및 서버 도입 비용도 절감할 수 있었다.&lt;/p&gt;</description>
      <category>개발/[ERP] 풀스택 AI개발</category>
      <category>HWPX문서 자동생성</category>
      <author>그린 파인애플</author>
      <guid isPermaLink="true">https://utlee0.tistory.com/13</guid>
      <comments>https://utlee0.tistory.com/13#entry13comment</comments>
      <pubDate>Fri, 4 Sep 2026 17:20:49 +0900</pubDate>
    </item>
    <item>
      <title>[ERP 구축기 #06] 반복되는 API 호출 줄이기 - Redis와 React Query 캐싱</title>
      <link>https://utlee0.tistory.com/12</link>
      <description>&lt;h3 data-end=&quot;1013&quot; data-start=&quot;984&quot; data-ke-size=&quot;size23&quot;&gt;  반복해서 조회되는 공통코드&lt;/h3&gt;
&lt;p data-end=&quot;1013&quot; data-start=&quot;984&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 ERP 시스템에서는 화면 곳곳에서 공통코드를 사용한다.&lt;/p&gt;
&lt;p data-end=&quot;1083&quot; data-start=&quot;1018&quot; data-ke-size=&quot;size16&quot;&gt;직급, 자격구분, 공종구분, 상태값 등과 같은 공통코드는 여러 화면에서 반복적으로 조회되지만 데이터가 자주 변경되지는 않는다.&lt;/p&gt;
&lt;p data-end=&quot;1183&quot; data-start=&quot;1088&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1183&quot; data-start=&quot;1088&quot; data-ke-size=&quot;size16&quot;&gt;개발하면서 확인해보니 화면을 이동하거나 새로고침할 때마다 동일한 공통코드를 API를 통해 조회하고 있었고, Backend에서도 매번 PostgreSQL을 조회하고 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1243&quot; data-start=&quot;1188&quot; data-ke-size=&quot;size16&quot;&gt;데이터 변경은 적은데 조회는 빈번하기 때문에 공통코드는 캐싱 효과를 보기 좋은 데이터라고 판단했다.&lt;/p&gt;
&lt;p data-end=&quot;1322&quot; data-start=&quot;1248&quot; data-ke-size=&quot;size16&quot;&gt;기존에 로그인 세션 관리 용도로 사용하고 있던 Redis가 있었기 때문에 공통코드 조회에도 Redis Cache를 적용해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.2558%;&quot;&gt;특징&lt;/td&gt;
&lt;td style=&quot;width: 61.6279%;&quot;&gt;공통코드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.2558%;&quot;&gt;조회 빈도&lt;/td&gt;
&lt;td style=&quot;width: 61.6279%;&quot;&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.2558%;&quot;&gt;데이터 변경&lt;/td&gt;
&lt;td style=&quot;width: 61.6279%;&quot;&gt;적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.2558%;&quot;&gt;여러 화면에서 재사용&lt;/td&gt;
&lt;td style=&quot;width: 61.6279%;&quot;&gt;많음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.2558%;&quot;&gt;데이터 크기&lt;/td&gt;
&lt;td style=&quot;width: 61.6279%;&quot;&gt;작음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.2558%;&quot;&gt;실시간성이 반드시 필요한가?&lt;/td&gt;
&lt;td style=&quot;width: 61.6279%;&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;b&gt;조회 빈도가 높고 변경 빈도가 낮은 공통코드부터 캐싱&lt;/b&gt;하기로 했다&lt;/p&gt;
&lt;p data-end=&quot;2107&quot; data-start=&quot;2057&quot; data-ke-size=&quot;size16&quot;&gt;Redis를 캐시로 사용하면서 가장 먼저 고려한 것은 Redis에 문제가 발생했을 때였다.&lt;/p&gt;
&lt;p data-end=&quot;2202&quot; data-start=&quot;2112&quot; data-ke-size=&quot;size16&quot;&gt;공통코드는 시스템 동작에 필요한 데이터이지만 원본 데이터는 PostgreSQL에 있기 때문에 Redis 장애가 전체 서비스 장애로 이어지면 안되기 때문에&lt;/p&gt;
&lt;p data-end=&quot;2202&quot; data-start=&quot;2112&quot; data-ke-size=&quot;size16&quot;&gt;Redis 조회가 실패하거나 1초내 응답하지 않는 경우 PostgreSQL에서 데이터를 조회하도록 fallback 로직을 구성했다.&lt;/p&gt;
&lt;p data-end=&quot;2288&quot; data-start=&quot;2207&quot; data-ke-size=&quot;size16&quot;&gt;현재 개발 단계에서는 1초를 기준으로 fallback하도록 구현했으며, 실제 운영 환경의 Redis 응답시간을 모니터링한 뒤 timeout 값은 조정할 예정이다.&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;1893&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YiZ45/dJMcaiR8WWT/JLPEG5eOY89wd2AISzlkgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YiZ45/dJMcaiR8WWT/JLPEG5eOY89wd2AISzlkgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YiZ45/dJMcaiR8WWT/JLPEG5eOY89wd2AISzlkgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYiZ45%2FdJMcaiR8WWT%2FJLPEG5eOY89wd2AISzlkgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1893&quot; height=&quot;903&quot; data-origin-width=&quot;1893&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  캐시와 DB 데이터의 정합성 유지&lt;/h3&gt;
&lt;p data-end=&quot;3016&quot; data-start=&quot;2960&quot; data-ke-size=&quot;size16&quot;&gt;캐싱을 적용하면서 성능보다 더 고민했던 부분은 &lt;b&gt;DB와 Redis의 데이터가 달라지는 문제&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-end=&quot;3094&quot; data-start=&quot;3021&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 관리자가 공통코드를 수정했는데 Redis에는 기존 값이 남아 있다면 사용자는 일정 시간 동안 잘못된 데이터를 보게 된다.&lt;/p&gt;
&lt;p data-end=&quot;3185&quot; data-start=&quot;3099&quot; data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 공통코드 등록&amp;middot;수정 API가 호출될 경우 PostgreSQL의 데이터를 변경하는 것과 함께 Redis 캐시도 갱신하도록 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 해보니 캐싱을 이용하는 방법이 몇가지가 있었다&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;명칭&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;DB 변경 후 Redis 처리&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;캐시 데이터 갱신후 다음 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;Cache Evict&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;캐시 삭제&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;DB 조회 후 다시 캐싱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;Cache Put&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;캐시를 최신 값으로 변경&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;바로 Redis 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;Cache Aside&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;캐시가 없을 때 DB 조회&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;DB 결과를 캐싱&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;p data-ke-size=&quot;size16&quot;&gt;Cache Evict + Aside만 적용하기로 했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;1377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdjP8h/dJMcabZUAJt/acGI5Q5N4Q9dPL8RgK9dz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdjP8h/dJMcabZUAJt/acGI5Q5N4Q9dPL8RgK9dz1/img.png&quot; data-alt=&quot;Cache Avict와 Aside 개념&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdjP8h/dJMcabZUAJt/acGI5Q5N4Q9dPL8RgK9dz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdjP8h%2FdJMcabZUAJt%2FacGI5Q5N4Q9dPL8RgK9dz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1142&quot; height=&quot;1377&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;1377&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Cache Avict와 Aside 개념&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 적용전에는 200ms~400ms 정도 였던 응답시간이 Redis 캐싱 적용후 절반 이상으로 줄었다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;509&quot; data-origin-height=&quot;509&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clboct/dJMcaf8OnbL/OlgLwpI3boWle2K2Rxc8GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clboct/dJMcaf8OnbL/OlgLwpI3boWle2K2Rxc8GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clboct/dJMcaf8OnbL/OlgLwpI3boWle2K2Rxc8GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fclboct%2FdJMcaf8OnbL%2FOlgLwpI3boWle2K2Rxc8GK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;509&quot; height=&quot;509&quot; data-origin-width=&quot;509&quot; data-origin-height=&quot;509&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXysAh/dJMcaaNlXWt/li4XlFkO9ErvmZY8MkmlfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXysAh/dJMcaaNlXWt/li4XlFkO9ErvmZY8MkmlfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXysAh/dJMcaaNlXWt/li4XlFkO9ErvmZY8MkmlfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXysAh%2FdJMcaaNlXWt%2Fli4XlFkO9ErvmZY8MkmlfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1220&quot; height=&quot;650&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;5180&quot; data-end=&quot;5216&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;5180&quot; data-end=&quot;5216&quot; data-ke-size=&quot;size16&quot;&gt;추가적으로&amp;nbsp; React query를 사용하면서 Cache의 유효시간은 10분으로 설정했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;5221&quot; data-end=&quot;5306&quot; data-ke-size=&quot;size16&quot;&gt;공통코드는 변경 빈도가 높지 않기 때문에 우선 10분으로 설정했으며, 운영 이후 코드 변경 빈도와 API 호출량을 모니터링하여 필요하면 조정할 예정이다.&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; ️ Backend뿐 아니라 Frontend에서도 한 번 더 캐싱&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;계층&lt;/td&gt;
&lt;td&gt;캐시&lt;/td&gt;
&lt;td&gt;목적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React Query&lt;/td&gt;
&lt;td&gt;동일 화면/컴포넌트의 불필요한 요청 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;sessionStorage&lt;/td&gt;
&lt;td&gt;새로고침 후에도 일정 시간 데이터 재사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;DB 조회 감소 및 API 응답속도 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;원본 데이터 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;342&quot; data-start=&quot;260&quot; data-ke-size=&quot;size16&quot;&gt;Backend에서 Redis Cache를 적용한 것과 별개로 Frontend에서도 동일한 공통코드 API가 반복적으로 호출되는 것을 줄이고 싶었다.&lt;/p&gt;
&lt;p data-end=&quot;342&quot; data-start=&quot;260&quot; data-ke-size=&quot;size16&quot;&gt;개발을 하면서 새로고침을 할일이 많았는데 그때마다 네트워크 탭을 보니 매번 API호출을 하고 있었고, 업무 화면 특성상 여러 탭을 띄워서 작업을 하기에 탭이 많은만큼 공통코드 API 도 여러번 호출하고 있었다&lt;/p&gt;
&lt;p data-end=&quot;342&quot; data-start=&quot;260&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;498&quot; data-start=&quot;344&quot; data-ke-size=&quot;size16&quot;&gt;React Query를 이용하면 이미 조회한 데이터를 메모리에 캐싱하여 여러 화면에서 동일한 API를 반복해서 호출하는 것을 줄일 수 있다. 하지만 브라우저를 새로고침하면 메모리에 저장된 데이터가 초기화되기 때문에 새로고침 이후에는 다시 API를 호출해야 하는 문제가 있었다.&lt;/p&gt;
&lt;p data-end=&quot;604&quot; data-start=&quot;500&quot; data-ke-size=&quot;size16&quot;&gt;공통코드는 자주 변경되는 데이터가 아니기 때문에 &lt;b&gt;sessionStorage에도 공통코드와 캐시 시간을 함께 저장하고, 10분 동안은 동일한 공통코드 API를 다시 호출하지 않도록 구성했다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 개선하니 브라우저를 새로고침해도 이미 캐싱되어서 브라우저에서 가지고 있는 상태라면 동일한 공통코드를 가져오기 위해 API 호출량을 줄일수 있었고, 10분후, 재로그인하여 화면 진입시 공통코드 API를 다시 호출하여 redis에서 최신화된 데이터를 가져올수 있게 되었다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐싱을 적용하면서 느낀 점&lt;/h3&gt;
&lt;p data-end=&quot;5643&quot; data-start=&quot;5589&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 로그인 세션 관리에만 사용하고 있던 Redis를 조금 더 활용해보자는 생각으로 시작했다.&lt;/p&gt;
&lt;p data-end=&quot;5753&quot; data-start=&quot;5648&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제로 적용해보니 단순히 Redis에 데이터를 저장하는 것보다 &lt;b&gt;어떤 데이터를 캐싱할 것인지와 원본 데이터가 변경되었을 때 캐시의 정합성을 어떻게 유지할 것인지가 더 중요했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;5843&quot; data-start=&quot;5758&quot; data-ke-size=&quot;size16&quot;&gt;또한 Backend의 Redis Cache만 적용하는 것보다 Frontend에서도 반복적인 API 호출을 줄이면서 전체적인 요청 횟수를 줄일 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;5976&quot; data-start=&quot;5848&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 모든 데이터를 무조건 캐싱하기보다는 &lt;b&gt;조회 빈도는 높고 변경 빈도는 낮은 데이터&lt;/b&gt;를 중심으로 캐싱 대상을 검토하고, Grafana 모니터링을 통해 실제 API 호출량과 응답시간을 확인하면서 적용 범위를 조정할 예정이다.&lt;/p&gt;</description>
      <category>개발/[ERP] 풀스택 AI개발</category>
      <author>그린 파인애플</author>
      <guid isPermaLink="true">https://utlee0.tistory.com/12</guid>
      <comments>https://utlee0.tistory.com/12#entry12comment</comments>
      <pubDate>Fri, 28 Aug 2026 17:02:02 +0900</pubDate>
    </item>
    <item>
      <title>[ERP 구축기 #05] 메뉴 숨김만으로 충분할까? ERP의 RBAC 권한 설계</title>
      <link>https://utlee0.tistory.com/11</link>
      <description>&lt;h3 data-end=&quot;919&quot; data-start=&quot;885&quot; data-section-id=&quot;j886s3&quot; data-ke-size=&quot;size23&quot;&gt;  ERP에서는 모든 사용자가 같은 기능을 사용할 수 없다&lt;/h3&gt;
&lt;p style=&quot;color: #666666; text-align: justify;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;923&quot; data-end=&quot;986&quot;&gt;ERP 시스템을 개발하다 보면 사용자에 따라 접근할 수 있는 메뉴와 수행할 수 있는 작업을 다르게 제어해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: justify;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;991&quot; data-end=&quot;1090&quot;&gt;예를 들어 같은 메뉴를 사용하더라도 일반 사용자는 조회만 가능하고, 업무 담당자는 데이터를 등록하거나 수정할 수 있으며, 관리자만 삭제할 수 있도록 권한을 구분해야 할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: justify;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;1095&quot; data-end=&quot;1195&quot;&gt;신규 PQ 시스템에서도 이러한 요구사항이 있었기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RBAC(Role-Based Access Control, 역할 기반 접근 제어)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식으로 권한 관리 기능을 설계했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;1798&quot; data-start=&quot;1710&quot; data-ke-size=&quot;size16&quot;&gt;관리의 편의성을 위해 기존 사용자 및 메뉴 정보와 연계하여&lt;b&gt;&amp;nbsp;DB에서 역할과 메뉴 권한을 관리하는 방식&lt;/b&gt;으로 구현하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2119&quot; data-start=&quot;2094&quot; data-section-id=&quot;exp6o9&quot; data-ke-size=&quot;size23&quot;&gt;  역할 기반 권한 + 사용자별 예외 권한&lt;/h3&gt;
&lt;p style=&quot;color: #666666; text-align: justify;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;2123&quot; data-end=&quot;2156&quot;&gt;기본적인 권한은 역할(Role)을 기준으로 관리하도록 했다.&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: justify;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;2161&quot; data-end=&quot;2245&quot;&gt;예를 들어 일반 사용자, 업무 담당자, 관리자와 같은 역할에 메뉴별 CRUD 권한을 부여하고 사용자는 해당 역할을 통해 권한을 가지게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: justify;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;2250&quot; data-end=&quot;2353&quot;&gt;다만 실제 ERP를 운영하다 보면 동일한 역할을 가진 사용자 중 특정 사용자에게만 추가 권한을 부여해야 하는 경우가 있기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;사용자별 예외 권한도 설정할 수 있도록 구성했다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: justify;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;2358&quot; data-end=&quot;2432&quot;&gt;&lt;br /&gt;부서별 권한도 검토했지만 현재 업무 규모에서는 역할 기반 권한만으로 충분하다고 판단하여 제외했고, 필요할 경우 추후 확장할 예정이다.&lt;/p&gt;
&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;1681&quot; data-origin-height=&quot;935&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dASOlx/dJMcadb40rh/JNLNZCSVUETcNxWkcTlCd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dASOlx/dJMcadb40rh/JNLNZCSVUETcNxWkcTlCd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dASOlx/dJMcadb40rh/JNLNZCSVUETcNxWkcTlCd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdASOlx%2FdJMcadb40rh%2FJNLNZCSVUETcNxWkcTlCd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1681&quot; height=&quot;935&quot; data-origin-width=&quot;1681&quot; data-origin-height=&quot;935&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;table style=&quot;border-collapse: collapse; width: 100%; height: 105px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 17.907%; height: 21px;&quot;&gt;의미&lt;/td&gt;
&lt;td style=&quot;width: 81.9767%; height: 21px;&quot;&gt;시스템 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 17.907%; height: 21px;&quot;&gt;등록&lt;/td&gt;
&lt;td style=&quot;width: 81.9767%; height: 21px;&quot;&gt;신규 데이터 등록 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 17.907%; height: 21px;&quot;&gt;조회&lt;/td&gt;
&lt;td style=&quot;width: 81.9767%; height: 21px;&quot;&gt;메뉴 접근 및 데이터 조회 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 17.907%; height: 21px;&quot;&gt;수정&lt;/td&gt;
&lt;td style=&quot;width: 81.9767%; height: 21px;&quot;&gt;기존 데이터 수정 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 17.907%; height: 21px;&quot;&gt;삭제&lt;/td&gt;
&lt;td style=&quot;width: 81.9767%; height: 21px;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계한 구조는 아래와 같다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;3254&quot; data-end=&quot;3321&quot; data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 Backend의 권한 API를 호출하여 현재 사용자가 사용할 수 있는 메뉴와 CRUD 권한을 조회한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;3326&quot; data-end=&quot;3393&quot; data-ke-size=&quot;size16&quot;&gt;Frontend에서는 조회한 권한 정보를 이용하여 사이드바 메뉴를 노출하고 업무 화면 진입시 권한에 따라 등록&amp;middot;수정&amp;middot;삭제등의 버튼을 활성화/비활성화한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;3398&quot; data-end=&quot;3447&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;3398&quot; data-end=&quot;3447&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Frontend에서 메뉴나 버튼을 숨기는 것만으로는 실제 보안을 보장할 수 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;3452&quot; data-end=&quot;3529&quot; data-ke-size=&quot;size16&quot;&gt;브라우저의 localStorage 값은 사용자가 직접 변경할 수 있고, 화면을 거치지 않고 API를 직접 호출하는 것도 가능하기 때문이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;3534&quot; data-end=&quot;3625&quot; data-ke-size=&quot;size16&quot;&gt;따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Frontend의 권한 제어는 사용자 편의를 위한 UX 용도로 사용하고, 실제 데이터 접근 권한은 Backend API에서 다시 검증하도록 구현했다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duq3h6/dJMcabSZ6d6/fQDjIyyVviVwIiThXpFw20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duq3h6/dJMcabSZ6d6/fQDjIyyVviVwIiThXpFw20/img.png&quot; data-alt=&quot;업무 화면에서의 권한 처리 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duq3h6/dJMcabSZ6d6/fQDjIyyVviVwIiThXpFw20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fduq3h6%2FdJMcabSZ6d6%2FfQDjIyyVviVwIiThXpFw20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1536&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;업무 화면에서의 권한 처리 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;3321&quot; data-start=&quot;3254&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj8SV6/dJMcacYIKqq/MCmFyGFgEa2dkb8KUeYxv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj8SV6/dJMcacYIKqq/MCmFyGFgEa2dkb8KUeYxv0/img.png&quot; data-alt=&quot;백엔드에서 API 요청을 받을때 권한 체크를 한번더 진행한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj8SV6/dJMcacYIKqq/MCmFyGFgEa2dkb8KUeYxv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj8SV6%2FdJMcacYIKqq%2FMCmFyGFgEa2dkb8KUeYxv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;백엔드에서 API 요청을 받을때 권한 체크를 한번더 진행한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;3321&quot; data-start=&quot;3254&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3321&quot; data-start=&quot;3254&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5164&quot; data-start=&quot;5109&quot; data-ke-size=&quot;size16&quot;&gt;권한 제어와 별개로 주요 API 호출에 대해서는 감사로그(Audit Log)를 기록하도록 구성했다.&lt;/p&gt;
&lt;p data-end=&quot;5282&quot; data-start=&quot;5169&quot; data-ke-size=&quot;size16&quot;&gt;누가, 언제, 어떤 API를 호출했는지 기록하여 문제가 발생했을 때 사용자 행위를 추적할 수 있도록 했으며, 비정상적인 사용 패턴이나 확인이 필요한 계정은 배치를 통해 별도로 확인할 수 있도록 구성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DhfIT/dJMcahMnS45/yZxMAUdHod1YUVUIQhXM9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DhfIT/dJMcahMnS45/yZxMAUdHod1YUVUIQhXM9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DhfIT/dJMcahMnS45/yZxMAUdHod1YUVUIQhXM9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDhfIT%2FdJMcahMnS45%2FyZxMAUdHod1YUVUIQhXM9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;503&quot; height=&quot;510&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한제어를 하는 화면의 구성은 아래와 같이 했고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편의를 위해 행, 특정 모듈의 메뉴는 한번에 전체 체크 가능하도록 구성했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mWglU/dJMcahscLN8/Q1fzCawysmDO3sy7aDg3JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mWglU/dJMcahscLN8/Q1fzCawysmDO3sy7aDg3JK/img.png&quot; data-alt=&quot;역할별 권한 관리 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mWglU/dJMcahscLN8/Q1fzCawysmDO3sy7aDg3JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmWglU%2FdJMcahscLN8%2FQ1fzCawysmDO3sy7aDg3JK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1899&quot; height=&quot;902&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;역할별 권한 관리 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GwQFj/dJMcafgOysN/WtwtOfUYwUpP8ikf0ULxm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GwQFj/dJMcafgOysN/WtwtOfUYwUpP8ikf0ULxm1/img.png&quot; data-alt=&quot;사용자별 개별 권한 제어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GwQFj/dJMcafgOysN/WtwtOfUYwUpP8ikf0ULxm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGwQFj%2FdJMcafgOysN%2FWtwtOfUYwUpP8ikf0ULxm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1899&quot; height=&quot;904&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사용자별 개별 권한 제어&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;869&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfMI0V/dJMcaf147XB/vwHuVqYqVwOeKmluCkbdwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfMI0V/dJMcaf147XB/vwHuVqYqVwOeKmluCkbdwk/img.png&quot; data-alt=&quot;권한에 따라 이용가능한 기능의 버튼 제어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfMI0V/dJMcaf147XB/vwHuVqYqVwOeKmluCkbdwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfMI0V%2FdJMcaf147XB%2FvwHuVqYqVwOeKmluCkbdwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1914&quot; height=&quot;869&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;869&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;h3 data-ke-size=&quot;size23&quot;&gt;  앞으로 추가할 보안 기능&amp;nbsp;&lt;/h3&gt;
&lt;p data-end=&quot;6248&quot; data-start=&quot;6205&quot; data-ke-size=&quot;size16&quot;&gt;현재는 메뉴 및 CRUD 권한을 중심으로 기본적인 접근 제어 기능을 구현했다.&lt;/p&gt;
&lt;p data-end=&quot;6352&quot; data-start=&quot;6253&quot; data-ke-size=&quot;size16&quot;&gt;앞으로는 이전 시스템 구축 및 ISMS-P 대응 경험을 활용하여 중복 로그인 제어, 개인정보 암호화, 비밀번호 정책 등 ERP 운영에 필요한 보안 기능을 단계적으로 추가할 예정이다.&lt;/p&gt;
&lt;p data-end=&quot;6406&quot; data-start=&quot;6357&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6406&quot; data-start=&quot;6357&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;6583&quot; data-start=&quot;6569&quot; data-section-id=&quot;1pba0im&quot; data-ke-size=&quot;size23&quot;&gt;  기능 구현하면서 느낀 점&lt;/h3&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-start=&quot;6587&quot; data-end=&quot;6692&quot; data-ke-size=&quot;size16&quot;&gt;처음 권한 기능을 설계할 때는 메뉴를 보여주거나 숨기는 정도로 생각했지만, 실제로 구현하다 보니 화면에서의 권한 제어와 서버에서의 권한 검증은 역할이 다르다는 것을 다시 확인할 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-start=&quot;6697&quot; data-end=&quot;6793&quot; data-ke-size=&quot;size16&quot;&gt;Frontend에서 메뉴나 버튼을 숨기는 것은 사용자가 사용할 수 없는 기능을 보여주지 않기 위한 UX이고, 실제 데이터에 대한 접근을 막는 것은 Backend의 역할이다.&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-start=&quot;6798&quot; data-end=&quot;6896&quot; data-ke-size=&quot;size16&quot;&gt;또한 권한을 지나치게 세분화하면 관리가 복잡해질 수 있기 때문에 현재 시스템에 필요한 수준에서 단순하게 시작하고, 실제 운영 과정에서 필요한 권한을 추가하는 방향으로 설계했다.&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-start=&quot;6901&quot; data-end=&quot;6992&quot; data-ke-size=&quot;size16&quot;&gt;ERP의 권한 설계에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;얼마나 많은 권한을 만들 것인가보다, 누가 어떤 데이터와 기능에 접근할 수 있는지를 명확하게 정의하는 것이 더 중요하다고 생각한다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>개발/[ERP] 풀스택 AI개발</category>
      <author>그린 파인애플</author>
      <guid isPermaLink="true">https://utlee0.tistory.com/11</guid>
      <comments>https://utlee0.tistory.com/11#entry11comment</comments>
      <pubDate>Fri, 28 Aug 2026 15:47:49 +0900</pubDate>
    </item>
    <item>
      <title>[ERP 구축기 #04] APM이 없어서 직접 만든 Grafana 모니터링 환경</title>
      <link>https://utlee0.tistory.com/10</link>
      <description>&lt;h3 data-end=&quot;874&quot; data-start=&quot;849&quot; data-section-id=&quot;1szgcx7&quot; data-ke-size=&quot;size23&quot;&gt;  기존 ERP에는 모니터링 환경이 없었다&lt;/h3&gt;
&lt;p data-end=&quot;989&quot; data-start=&quot;878&quot; data-ke-size=&quot;size16&quot;&gt;기존 ERP 시스템을 운영하면서 확인해보니 애플리케이션의 상태를 통합적으로 확인할 수 있는 APM(Application Performance Monitoring) 환경이 별도로 구축되어 있지 않았다.&lt;/p&gt;
&lt;p data-end=&quot;989&quot; data-start=&quot;878&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1068&quot; data-start=&quot;994&quot; data-ke-size=&quot;size16&quot;&gt;시스템에 장애가 발생하거나 특정 API의 응답이 느려지더라도 &lt;b&gt;어느 구간에서 문제가 발생했는지 빠르게 확인하기 어려운 구조&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-end=&quot;1168&quot; data-start=&quot;1073&quot; data-ke-size=&quot;size16&quot;&gt;신규 PQ 시스템을 구축하면서 단순히 서비스를 개발하는 것에서 끝내지 않고, 운영 단계에서 발생하는 문제까지 빠르게 추적할 수 있는 모니터링 환경을 함께 구축하기로 했다.&lt;/p&gt;
&lt;p data-end=&quot;1168&quot; data-start=&quot;1073&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1250&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;상용 APM을 도입하는 방법도 있지만 비용 부담이 있기 때문에 우선 Self-host 방식으로 운영할 수 있는 오픈소스 솔루션을 검토했다.&lt;/p&gt;
&lt;p data-end=&quot;1250&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그로부터 시일이 꽤 지나서 무료로 사용할 수 있는 APM이 무엇이 있나 확인을 해보았고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;grafana외 여러 제품군이 있다는걸 알게 되었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1810&quot; data-start=&quot;1795&quot; data-section-id=&quot;nw2556&quot; data-ke-size=&quot;size23&quot;&gt;  오픈소스 APM 비교&lt;/h3&gt;
&lt;blockquote data-end=&quot;2036&quot; data-start=&quot;1812&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;p data-end=&quot;1853&quot; data-start=&quot;1814&quot; data-ke-size=&quot;size16&quot;&gt;신규 시스템에 적용할 모니터링 솔루션은 다음 조건을 중심으로 검토했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2036&quot; data-start=&quot;1858&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1886&quot; data-start=&quot;1858&quot; data-section-id=&quot;1ew8mel&quot;&gt;Self-host 방식으로 무료 운영이 가능한가&lt;/li&gt;
&lt;li data-end=&quot;1912&quot; data-start=&quot;1889&quot; data-section-id=&quot;vlq2ye&quot;&gt;Spring Boot와 연동하기 쉬운가&lt;/li&gt;
&lt;li data-end=&quot;1942&quot; data-start=&quot;1915&quot; data-section-id=&quot;1dthel8&quot;&gt;API 요청에 대한 Trace 추적이 가능한가&lt;/li&gt;
&lt;li data-end=&quot;1972&quot; data-start=&quot;1945&quot; data-section-id=&quot;1qx2mxe&quot;&gt;SQL 및 DB 성능 문제를 추적할 수 있는가&lt;/li&gt;
&lt;li data-end=&quot;1998&quot; data-start=&quot;1975&quot; data-section-id=&quot;y9tdu8&quot;&gt;로그와 메트릭을 함께 확인할 수 있는가&lt;/li&gt;
&lt;li data-end=&quot;2036&quot; data-start=&quot;2001&quot; data-section-id=&quot;1wa4bg1&quot;&gt;운영에 필요한 Dashboard를 자유롭게 구성할 수 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 115px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; 제품 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;무료 Self-host&lt;/td&gt;
&lt;td&gt;Spring Boot&lt;/td&gt;
&lt;td&gt;SQL 추적&lt;/td&gt;
&lt;td&gt;UI 편의성추천&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;SigNoz&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅ 매우 좋음&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;1순위&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Uptrace&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;2순위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Grafana + Tempo&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;⭐⭐⭐&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;기존 Grafana 활용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Apache SkyWalking&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅ 매우 좋음&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;⭐⭐⭐&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Java 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Jaeger&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;⭐⭐&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Trace 전용에 가까움&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2567&quot; data-start=&quot;2490&quot; data-ke-size=&quot;size16&quot;&gt;Docker를 활용해 비교할 APM들을 직접 구축하고 현재 개발 중인 Spring Boot 서비스와 연동해보면서 생각이 조금 달라졌다.&lt;/p&gt;
&lt;p data-end=&quot;2654&quot; data-start=&quot;2572&quot; data-ke-size=&quot;size16&quot;&gt;Grafana는 기존에 사용해본 경험이 있었고, Dashboard를 업무 및 운영 환경에 맞게 자유롭게 구성할 수 있다는 점이 가장 큰 장점이었다.&lt;/p&gt;
&lt;p data-end=&quot;2654&quot; data-start=&quot;2572&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2788&quot; data-start=&quot;2659&quot; data-ke-size=&quot;size16&quot;&gt;또한 Prometheus, Loki, Tempo 등을 조합하면 &lt;b&gt;Metrics, Logs, Traces를 하나의 Grafana 화면에서 연결하여 확인할 수 있기 때문에&lt;/b&gt; 최종적으로 Grafana Stack을 사용하기로 결정했다.&lt;/p&gt;
&lt;p data-end=&quot;2788&quot; data-start=&quot;2659&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; 구성요소 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Grafana&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Metrics / Logs / Traces 통합 시각화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Prometheus&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Spring Boot 및 시스템 Metrics 수집&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Loki&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Spring Boot 애플리케이션 로그 저장 및 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Tempo&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Trace ID 기반 API 요청 흐름 추적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Grafana Alloy&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;애플리케이션 로그 수집 및 Loki 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Node Exporter&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;물리 서버 CPU, Memory, Disk 등 수집&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Redis Exporter&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Redis Metrics 수집&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Postgres Exporter&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL Metrics 수집&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  Grafana 모니터링을 위한 아키텍쳐&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0RZix/dJMcadQLHGb/Kk2niMZkhfd6zvcEWsW8d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0RZix/dJMcadQLHGb/Kk2niMZkhfd6zvcEWsW8d0/img.png&quot; data-alt=&quot;Grafana 로그수집 아키텍쳐&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0RZix/dJMcadQLHGb/Kk2niMZkhfd6zvcEWsW8d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0RZix%2FdJMcadQLHGb%2FKk2niMZkhfd6zvcEWsW8d0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana 로그수집 아키텍쳐&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5779&quot; data-start=&quot;5696&quot; data-ke-size=&quot;size16&quot;&gt;단순히 CPU와 Memory 현황 그래프를 보는 것보다 실제 장애가 발생했을 때 &lt;b&gt;원인을 빠르게 찾을 수 있는 구조를 만드는 것&lt;/b&gt;을 중요하게 생각했다.&lt;/p&gt;
&lt;p data-end=&quot;5779&quot; data-start=&quot;5696&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5914&quot; data-start=&quot;5784&quot; data-ke-size=&quot;size16&quot;&gt;Dashboard에서 오류가 발생한 API를 확인하면 Trace ID를 클릭하여 Tempo로 이동하고, 해당 요청이 Controller &amp;rarr; Service &amp;rarr; Repository 등 어떤 경로로 처리되었는지 확인할 수 있도록 구성했다.&lt;/p&gt;
&lt;p data-end=&quot;6010&quot; data-start=&quot;5919&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6010&quot; data-start=&quot;5919&quot; data-ke-size=&quot;size16&quot;&gt;추가적인 애플리케이션 로그가 필요한 경우 URI를 통해 Loki로 이동하여 해당 요청과 관련된 상세 로그를 확인할 수 있도록 Dashboard 간 링크도 연결했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8g1lg/dJMcadXC6st/E9VEJ3LBOKP6tUKXH3lVnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8g1lg/dJMcadXC6st/E9VEJ3LBOKP6tUKXH3lVnk/img.png&quot; data-alt=&quot;Grafana에서 대시보드에 있는 traceId 기반으로 빠르게 조회할수 있도록 연계한 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8g1lg/dJMcadXC6st/E9VEJ3LBOKP6tUKXH3lVnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8g1lg%2FdJMcadXC6st%2FE9VEJ3LBOKP6tUKXH3lVnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana에서 대시보드에 있는 traceId 기반으로 빠르게 조회할수 있도록 연계한 플로우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(Prometheus) Metrics&lt;/b&gt; &amp;mdash; CPU, Memory, API 응답시간, JVM, DB Connection 등 시스템의 상태를 수치로 확인&lt;br /&gt;&lt;b&gt;(Loki) Logs&lt;/b&gt; &amp;mdash; 애플리케이션에서 발생한 오류와 실행 기록 확인&lt;br /&gt;&lt;b&gt;(Tempo) Traces&lt;/b&gt; &amp;mdash; 하나의 API 요청이 애플리케이션 내부에서 어떻게 처리되었는지 추적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAAprW/dJMcaiEA3fn/1lbO9rNkPDlTTFDQAdfPQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAAprW/dJMcaiEA3fn/1lbO9rNkPDlTTFDQAdfPQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAAprW/dJMcaiEA3fn/1lbO9rNkPDlTTFDQAdfPQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAAprW%2FdJMcaiEA3fn%2F1lbO9rNkPDlTTFDQAdfPQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1840&quot; height=&quot;836&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3hvhj/dJMcaf8Xxth/ZJeUNYWIJnht9YXyqwiVs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3hvhj/dJMcaf8Xxth/ZJeUNYWIJnht9YXyqwiVs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3hvhj/dJMcaf8Xxth/ZJeUNYWIJnht9YXyqwiVs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3hvhj%2FdJMcaf8Xxth%2FZJeUNYWIJnht9YXyqwiVs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1840&quot; height=&quot;626&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1839&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SqDF9/dJMcacED0VV/uXtzHqdClNQpkhqW7ir2Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SqDF9/dJMcacED0VV/uXtzHqdClNQpkhqW7ir2Pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SqDF9/dJMcacED0VV/uXtzHqdClNQpkhqW7ir2Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSqDF9%2FdJMcacED0VV%2FuXtzHqdClNQpkhqW7ir2Pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1839&quot; height=&quot;1024&quot; data-origin-width=&quot;1839&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u3SuJ/dJMcabFxmYh/TdKuO4t77a5gKDBNvokNzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u3SuJ/dJMcabFxmYh/TdKuO4t77a5gKDBNvokNzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u3SuJ/dJMcabFxmYh/TdKuO4t77a5gKDBNvokNzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu3SuJ%2FdJMcabFxmYh%2FTdKuO4t77a5gKDBNvokNzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1836&quot; height=&quot;584&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;584&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;Tempo로 분산 Trace 데이터를 저장하고 Trace ID를 기준으로 API 요청의 처리 흐름과 각 Span의 수행시간을 추적하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenTelemetry를 통해 DB 호출 Span을 수집하여 API 처리 로직중 redis 조회, DB쿼리, API 연계등 실행 시간 차지하는 비중도 확인할 수 있도록 구성했고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 실행된 쿼리도 확인할 수 있도록 postProcessAfterInitialization 을 활용하여 실행된 완성 쿼리를 tempo에서 수집되게 구성했다&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;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmXxdN/dJMcagGN59J/u3hcTfECgIgllzwevcEgcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmXxdN/dJMcagGN59J/u3hcTfECgIgllzwevcEgcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmXxdN/dJMcagGN59J/u3hcTfECgIgllzwevcEgcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmXxdN%2FdJMcagGN59J%2Fu3hcTfECgIgllzwevcEgcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgZimA/dJMb998Dbfg/KhgjwMR2wCp38QkMPbQutk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgZimA/dJMb998Dbfg/KhgjwMR2wCp38QkMPbQutk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgZimA/dJMb998Dbfg/KhgjwMR2wCp38QkMPbQutk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgZimA%2FdJMb998Dbfg%2FKhgjwMR2wCp38QkMPbQutk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1821&quot; data-origin-height=&quot;891&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tlVYa/dJMcag0XzSK/yK2ZcmSA8rgakznOngQxN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tlVYa/dJMcag0XzSK/yK2ZcmSA8rgakznOngQxN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tlVYa/dJMcag0XzSK/yK2ZcmSA8rgakznOngQxN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtlVYa%2FdJMcag0XzSK%2FyK2ZcmSA8rgakznOngQxN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1821&quot; height=&quot;891&quot; data-origin-width=&quot;1821&quot; data-origin-height=&quot;891&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-end=&quot;7240&quot; data-start=&quot;7194&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7240&quot; data-start=&quot;7194&quot; data-ke-size=&quot;size16&quot;&gt;  처음에는 서버의 CPU나 Memory 사용량을 확인하는 정도의 모니터링을 생각했다.&lt;/p&gt;
&lt;p data-end=&quot;7311&quot; data-start=&quot;7245&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제로 구축해보니 운영에서 더 중요한 것은 &lt;b&gt;장애가 발생했을 때 원인을 얼마나 빠르게 찾을 수 있는가&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-end=&quot;7311&quot; data-start=&quot;7245&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7455&quot; data-start=&quot;7316&quot; data-ke-size=&quot;size16&quot;&gt;Metrics만으로는 시스템에 문제가 있다는 사실은 알 수 있지만 정확한 원인을 찾기는 어렵다. 여기에 Logs와 Traces를 연결하면서 특정 API에서 오류가 발생했을 때 요청 흐름과 관련 로그까지 빠르게 추적할 수 있는 환경을 만들 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;7547&quot; data-start=&quot;7460&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 PQ 시스템을 k3s 환경으로 만들면서 컨테이너와 Pod 상태까지 함께 모니터링하고, CI/CD 및 무중단 배포 환경도 단계적으로 구축할 예정이다.&lt;/p&gt;</description>
      <category>개발/[ERP] 풀스택 AI개발</category>
      <author>그린 파인애플</author>
      <guid isPermaLink="true">https://utlee0.tistory.com/10</guid>
      <comments>https://utlee0.tistory.com/10#entry10comment</comments>
      <pubDate>Wed, 26 Aug 2026 18:06:22 +0900</pubDate>
    </item>
    <item>
      <title>[ERP 구축기 #03] 현업이 AI로 만든 업무 도구를 ERP 정식 기능으로 발전시키기</title>
      <link>https://utlee0.tistory.com/9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 구축을 위해 업무 담당자 인터뷰중 PQ업무 중복도에 대해 간략한 이야기를 풀어볼까 한다&lt;/p&gt;
&lt;blockquote data-end=&quot;1052&quot; data-start=&quot;766&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;p data-end=&quot;838&quot; data-start=&quot;768&quot; data-ke-size=&quot;size16&quot;&gt;PQ업무 중복도란?&lt;br /&gt;&lt;br /&gt;PQ 업무에서는 입찰에 참여할 기술인이 현재 다른 프로젝트에 참여하고 있는지, 신규 프로젝트에 투입 가능한지를 확인해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;918&quot; data-start=&quot;843&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 특정 기술인이 현재 수행 중인 프로젝트와 신규 프로젝트의 참여 기간이 겹친다면 해당 기술인을 바로 투입하기 어려울 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1009&quot; data-start=&quot;923&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 참여 중인 프로젝트의 기간과 진척상황, 기술인의 투입 일정을 확인하여 프로젝트 간 업무가 중복되지 않는지를 관리하는 과정&lt;/b&gt;이 필요하고 회사에서는 이러한 업무를 업무중복도라는 이름으로 관리하고 있었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1367&quot; data-start=&quot;1337&quot; data-ke-size=&quot;size16&quot;&gt;현업 인터뷰를 진행하면서 새로운 점을 하나 발견했다.&lt;/p&gt;
&lt;p data-end=&quot;1457&quot; data-start=&quot;1372&quot; data-ke-size=&quot;size16&quot;&gt;업무 담당자들이 기존 시스템의 불편함을 해결하기 위해 &lt;b&gt;Claude AI를 활용하여 직접 간단한 HTML 기반 업무 도구를 만들어 사용하고 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1524&quot; data-start=&quot;1462&quot; data-ke-size=&quot;size16&quot;&gt;기존에 Excel로 관리하던 업무중복도 데이터를 업로드하면 브라우저에서 내용을 확인할 수 있도록 만든 도구였다.&lt;/p&gt;
&lt;p data-end=&quot;1597&quot; data-start=&quot;1529&quot; data-ke-size=&quot;size16&quot;&gt;개발자가 아닌 내부에서 직접 AI를 활용해 업무 도구를 만들어 실제 업무에 사용하고 있다는 점이 상당히 인상적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2173&quot; data-start=&quot;2115&quot; data-ke-size=&quot;size16&quot;&gt;ERP를 개발하면서 사용자가 시스템 밖에서 어떤 도구를 사용하고 있는지 확인하는 것도 중요하다고 느꼈다.&lt;/p&gt;
&lt;p data-end=&quot;2303&quot; data-start=&quot;2178&quot; data-ke-size=&quot;size16&quot;&gt;현업에서 별도의 Excel이나 자체 프로그램을 만들어 사용하고 있다면 단순한 개인 업무 방식으로 보기보다,&lt;/p&gt;
&lt;p data-end=&quot;2303&quot; data-start=&quot;2178&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 시스템에서 해결하지 못하고 있는 요구사항이 무엇인지 확인할 수 있는 중요한 단서&lt;/b&gt;가 될 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;883&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UbLXh/dJMb991VVdv/Ryue96c7pVbdeA0WIk1HhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UbLXh/dJMb991VVdv/Ryue96c7pVbdeA0WIk1HhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UbLXh/dJMb991VVdv/Ryue96c7pVbdeA0WIk1HhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUbLXh%2FdJMb991VVdv%2FRyue96c7pVbdeA0WIk1HhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1632&quot; height=&quot;883&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;883&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;업무 담당자가 위 시스템을? 사용해오면서 엑셀의 한계와 불편했던점, 내가 생각했을때 개선 포인트등을 직접 제안하고 아이디어를 내면서 업무중복도 관리 메뉴를 통합하여 기존에 6개의 메뉴로 나뉘어있던 화면을 2개로 통합 &amp;amp; 기획하여 한눈에 볼수 있도록 개발 하게 되었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 원본 데이터가 Excel 이기에&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 data-end=&quot;2711&quot; data-start=&quot;2693&quot; data-section-id=&quot;14oxchq&quot;&gt;Excel을 계속 관리해야 함&lt;/li&gt;
&lt;li data-end=&quot;2732&quot; data-start=&quot;2712&quot; data-section-id=&quot;14ewsg8&quot;&gt;데이터를 변경할 때마다 다시 업로드해야 함&lt;/li&gt;
&lt;li data-end=&quot;2759&quot; data-start=&quot;2733&quot; data-section-id=&quot;1lg8vb1&quot;&gt;여러 사용자가 동일 데이터를 공유하기 어려움&lt;/li&gt;
&lt;li data-end=&quot;2781&quot; data-start=&quot;2760&quot; data-section-id=&quot;4lwerp&quot;&gt;최신 데이터 여부를 확인하기 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 단순히 기존 HTML 화면을 그대로 PQ 시스템에 옮기는 것이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ERP에서 관리하는 프로젝트와 기술인 데이터를 이용해 업무중복도를 확인할 수 있는 구조로 통합하기로 했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;기존&lt;/td&gt;
&lt;td&gt;개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 관리&lt;/td&gt;
&lt;td&gt;Excel 중심&lt;/td&gt;
&lt;td&gt;ERP DB 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 반영&lt;/td&gt;
&lt;td&gt;Excel 수정 후 재업로드&lt;/td&gt;
&lt;td&gt;시스템에서 즉시 반영&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;화면 구성&lt;/td&gt;
&lt;td&gt;6개 메뉴&lt;/td&gt;
&lt;td&gt;2개 화면&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정보 확인&lt;/td&gt;
&lt;td&gt;메뉴별 개별 확인&lt;/td&gt;
&lt;td&gt;한 화면에서 통합 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 공유&lt;/td&gt;
&lt;td&gt;파일 중심&lt;/td&gt;
&lt;td&gt;시스템 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자&lt;/td&gt;
&lt;td&gt;PQ 담당자 중심&lt;/td&gt;
&lt;td&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjI2p6/dJMcacRQdDA/52wldHvIdyLdomB8OlmqHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjI2p6/dJMcacRQdDA/52wldHvIdyLdomB8OlmqHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjI2p6/dJMcacRQdDA/52wldHvIdyLdomB8OlmqHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjI2p6%2FdJMcacRQdDA%2F52wldHvIdyLdomB8OlmqHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-end=&quot;3711&quot; data-start=&quot;3687&quot; data-section-id=&quot;pvqr0i&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;3711&quot; data-start=&quot;3687&quot; data-section-id=&quot;pvqr0i&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;3711&quot; data-start=&quot;3687&quot; data-section-id=&quot;pvqr0i&quot; data-ke-size=&quot;size23&quot;&gt;실제 사용하는 여러 연관 부서의 의견도 확인&lt;/h3&gt;
&lt;p data-end=&quot;3762&quot; data-start=&quot;3715&quot; data-ke-size=&quot;size16&quot;&gt;업무중복도 화면을 기획&amp;amp;설계 하면서 PQ 서류를 작성하는 담당자와 실제 프로젝트에 참여하는 기술인과 프로젝트 일정을 관리하는 부서에서도 해당 정보를 활용하기 때문에 관련 부서의 의견도 함께 확인했다.&lt;/p&gt;
&lt;p data-end=&quot;3762&quot; data-start=&quot;3715&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3927&quot; data-start=&quot;3846&quot; data-ke-size=&quot;size16&quot;&gt;같은 데이터라도 PQ 담당자와 실제 프로젝트 관리자가 보고 싶은 정보와 사용하는 방식이 달랐기 때문에 여러 사용자 관점에서 화면을 다시 검토했다.&lt;/p&gt;
&lt;p data-end=&quot;3927&quot; data-start=&quot;3846&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;3927&quot; data-start=&quot;3846&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 말로 설명하기보다 화면으로 보면서 논의&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;4315&quot; data-start=&quot;4252&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 인터뷰를 통해 요구사항을 정리했지만, 업무 화면을 말로만 설명하면서 의견을 맞추는 데에는 한계가 있었다.&lt;/p&gt;
&lt;p data-end=&quot;4395&quot; data-start=&quot;4320&quot; data-ke-size=&quot;size16&quot;&gt;그래서 인터뷰 내용을 기반으로 AI를 활용하여 실제 동작과 유사한 퍼블리싱 화면을 먼저 만들고, 담당자와 화면을 함께 보면서 요구사항을 다시 확인했다.&lt;/p&gt;
&lt;p data-end=&quot;4412&quot; data-start=&quot;4400&quot; data-ke-size=&quot;size16&quot;&gt;실제 화면을 보여주면서&lt;/p&gt;
&lt;p data-end=&quot;4504&quot; data-start=&quot;4417&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;이 정보가 여기 있는 게 맞나요?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;4504&quot; data-start=&quot;4417&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;이 정보는 어느 화면에서 불러온 데이터를 출력하면 될까요?&quot;&lt;br /&gt;&quot;이 조건으로 검색하는 게 더 편하지 않을까요?&quot;&lt;br /&gt;&quot;이 두 화면은 하나로 합쳐도 되지 않을까요?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;4567&quot; data-start=&quot;4509&quot; 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;1898&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eoPwEw/dJMcaikffmI/VLrRAjLFKKVJITFg11UFik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eoPwEw/dJMcaikffmI/VLrRAjLFKKVJITFg11UFik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eoPwEw/dJMcaikffmI/VLrRAjLFKKVJITFg11UFik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeoPwEw%2FdJMcaikffmI%2FVLrRAjLFKKVJITFg11UFik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1898&quot; height=&quot;894&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;894&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1901&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtsYVA/dJMcadJUqYz/7KV9y2WcEuBZnnVKrpCniK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtsYVA/dJMcadJUqYz/7KV9y2WcEuBZnnVKrpCniK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtsYVA/dJMcadJUqYz/7KV9y2WcEuBZnnVKrpCniK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtsYVA%2FdJMcadJUqYz%2F7KV9y2WcEuBZnnVKrpCniK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1901&quot; height=&quot;887&quot; data-origin-width=&quot;1901&quot; data-origin-height=&quot;887&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;h3 data-end=&quot;6203&quot; data-start=&quot;6182&quot; data-section-id=&quot;10oyz1m&quot; data-ke-size=&quot;size23&quot;&gt;업무를 진행하면서 느낀 점&lt;/h3&gt;
&lt;p data-end=&quot;6310&quot; data-start=&quot;6208&quot; data-ke-size=&quot;size16&quot;&gt;이번 업무중복도 기능을 기획하면서 개발자가 요구사항을 그대로 받아 구현하는 것보다 &lt;b&gt;사용자가 현재 어떻게 업무를 처리하고 있는지 직접 확인하는 것이 중요하다는 것을 다시 느꼈다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;6310&quot; data-start=&quot;6208&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6416&quot; data-start=&quot;6315&quot; data-ke-size=&quot;size16&quot;&gt;특히 현업 담당자가 Excel과 AI를 이용해 직접 업무 도구를 만들어 사용하고 있다는 사실은 기존 시스템에서 해당 업무에 대한 요구가 충분히 해결되지 않고 있다는 중요한 신호였다.&lt;/p&gt;
&lt;p data-end=&quot;6497&quot; data-start=&quot;6421&quot; data-ke-size=&quot;size16&quot;&gt;또한 사용자가 요청한 6개의 기존 기능을 그대로 웹으로 옮기는 대신 실제 업무 흐름을 다시 확인하면서 2개의 화면으로 통합할 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;6497&quot; data-start=&quot;6421&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6564&quot; data-start=&quot;6502&quot; data-ke-size=&quot;size16&quot;&gt;요구사항을 말로만 논의하기보다 실제 화면을 빠르게 만들어 사용자와 함께 확인하는 방식도 생각보다 효과가 좋았다.&lt;/p&gt;
&lt;p data-end=&quot;6667&quot; data-start=&quot;6569&quot; data-ke-size=&quot;size16&quot;&gt;결국 ERP 재구축은 &lt;b&gt;기존 프로그램을 새로운 기술로 다시 만드는 작업이 아니라, 기존 업무 방식 자체를 다시 살펴보고 더 나은 방법으로 재설계하는 과정&lt;/b&gt;이라고 생각한다.&lt;/p&gt;</description>
      <category>개발/[ERP] 풀스택 AI개발</category>
      <author>그린 파인애플</author>
      <guid isPermaLink="true">https://utlee0.tistory.com/9</guid>
      <comments>https://utlee0.tistory.com/9#entry9comment</comments>
      <pubDate>Wed, 26 Aug 2026 18:00:17 +0900</pubDate>
    </item>
    <item>
      <title>[ERP 구축기 #02] &amp;quot;경력사항 입력만 하다 퇴근합니다&amp;quot; - 현업 인터뷰에서 시작된 업무 자동화</title>
      <link>https://utlee0.tistory.com/8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;ERP 시스템을 구축하면서 가장 중요하다고 느끼는 것은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;업무에 대한 이해와 데이터&lt;/span&gt;&lt;/b&gt;&lt;span&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최신 기술을 잘 활용하는 것도 중요하지만, 기술은 결국 실제 업무에서 발생하는 문제를 해결하기 위한 도구라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;기존 시스템의 기능을 그대로 웹으로 옮기는 것만으로는 현재 업무의 문제를 해결할 수 없다&lt;/b&gt;고 판단했고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; 실제 PQ 업무를 담당하는 실무자들과 인터뷰를 진행하면서 현재 업무가 어떻게 이루어지고 있는지부터 확인해보기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현업 인터뷰에서는 아래 항목을 중심으로 현재 업무 프로세스와 개선이 필요한 부분을 확인했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;현재&amp;nbsp;업무는&amp;nbsp;어떤&amp;nbsp;순서로&amp;nbsp;진행되는가? &lt;br /&gt;2.&amp;nbsp;기존&amp;nbsp;PQ&amp;nbsp;시스템은&amp;nbsp;실제로&amp;nbsp;어떻게&amp;nbsp;사용하고&amp;nbsp;있는가? &lt;br /&gt;3.&amp;nbsp;시스템&amp;nbsp;밖에서&amp;nbsp;Excel로&amp;nbsp;처리하는&amp;nbsp;업무는&amp;nbsp;무엇인가? &lt;br /&gt;4.&amp;nbsp;사람이&amp;nbsp;직접&amp;nbsp;입력하고&amp;nbsp;있는&amp;nbsp;데이터는&amp;nbsp;무엇인가? &lt;br /&gt;5.&amp;nbsp;반복적으로&amp;nbsp;수행하는&amp;nbsp;업무는&amp;nbsp;무엇인가? &lt;br /&gt;6.&amp;nbsp;업무하면서&amp;nbsp;가장&amp;nbsp;불편한&amp;nbsp;부분은&amp;nbsp;무엇인가? &lt;br /&gt;7.&amp;nbsp;신규&amp;nbsp;시스템에서&amp;nbsp;가장&amp;nbsp;개선되었으면&amp;nbsp;하는&amp;nbsp;것은&amp;nbsp;무엇인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;  인터뷰에서 발견한 가장 큰 문제는 '수작업'&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인터뷰를 시작하기 전에는 오래된 시스템의 기능 부족이나 UI/UX에 대한 불편사항이 가장 많이 나올 것이라고 예상했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 실제 담당자들과 이야기를 해보니 더 큰 문제는 따로 있었는데&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 시스템에서 지원하지 않아 수작업으로 처리하고 있는 업무가 생각보다 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일부 데이터는 Excel로 별도 관리되고 있었고, 시스템과 Excel 사이에서 데이터를 다시 입력해야 하는 업무도 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이러한 수작업은 단순히 업무 시간이 오래 걸리는 문제로 끝나지 않았는데&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;담당자 개인이 관리하는 데이터가 생기면서 &lt;u&gt;업무 공유가 어려워지고&lt;/u&gt;, 입찰 마감이 가까워질수록 &lt;u&gt;반복적인 입력 작업 때문에 야근&lt;/u&gt;까지 발생하고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이때부터 신규 시스템의 목표를 단순히 기존 기능을 웹으로 전환하는 것에서 끝내지 않고, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; 기존 업무에서 사람이 반복적으로 처리하고 있는 작업을 최대한 시스템화하는 것으로 목표를 잡게 되었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&quot;경력사항 입력만 하다가 집에 갈 때도 있어요&quot;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인터뷰 중 가장 기억에 남았던 이야기가 하나 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;PQ 서류를 작성하려면 프로젝트에 참여할 기술인들의 경력정보가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;건설기술인협회에서 발급받은 기술인 경력증명서에는 기본정보뿐만 아니라 자격, 학력, 교육, 경력 등 상당히 많은 정보가 포함되어 있는데, 기존 업무에서는 필요한 내용을 사람이 직접 확인하면서 시스템이나 Excel에 입력하고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;참여 기술인이 많아지면 입력해야 할 데이터도 매우 많이 늘어나고, 사람이 직접 입력하다 보니 오타나 누락이 발생할 가능성도 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;업무 담당자가 인터뷰 중 이런 이야기를 했다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&quot;경력사항 타이핑만 하다가 집에 갈 때도 있어요. 심지어 야근까지 할때도 있고요&quot;&lt;/span&gt;&lt;/p&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;&lt;span&gt;이 이야기를 듣고 신규 시스템이 완성될 때까지 기다리기보다, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;현재 업무에서 바로 사용할 수 있는 자동화 도구부터 만들어보는 것이 효과가 크겠다고 생각했다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;당장 업무에 적용할 수 있는 수준의 도구가 필요했기 때문에 Python과 AI 개발 도구를 활용해 2시간안에 빠르게 프로토타입을 만들었다.&lt;/p&gt;
&lt;p data-end=&quot;2163&quot; data-start=&quot;2060&quot; data-ke-size=&quot;size16&quot;&gt;Python을 이용해 기술인 경력증명서 PDF를 특정 폴더에 모두 넣고 필요한 데이터를 추출하고, &lt;b&gt;기본정보 / 자격 / 학력 / 교육 / 경력&lt;/b&gt;으로 구분하여 각각 Excel 시트로 생성하도록 구현했다.&lt;/p&gt;
&lt;p data-end=&quot;2273&quot; data-start=&quot;2168&quot; data-ke-size=&quot;size16&quot;&gt;사용 방법도 최대한 단순하게 만들었다. PQ에 참여할 기술인들의 PDF 파일을 지정된 폴더에 넣고 프로그램을 실행하면 여러 PDF를 일괄 분석하여 하나의 Excel 파일로 생성하도록 했다.&lt;/p&gt;
&lt;p data-end=&quot;2363&quot; data-start=&quot;2278&quot; data-ke-size=&quot;size16&quot;&gt;또한 별도의 Python 환경을 설치하지 않아도 사용할 수 있도록 EXE 형태로 패키징하여 IT 지식이 없는 업무 담당자도 쉽게 사용할 수 있도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 데이터를 추출해 보여주자 담당자가 &lt;b&gt;&quot;이제 이 업무 때문에 야근은 안 해도 되겠다&quot;&lt;/b&gt;며 매우 좋아했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3173&quot; data-start=&quot;3113&quot; data-ke-size=&quot;size16&quot;&gt;이번에 만든 Python 프로그램은 당장의 반복 업무를 줄이고 자동화 가능성을 검증하기 위한 프로토타입이다.&lt;/p&gt;
&lt;p data-end=&quot;3322&quot; data-start=&quot;3178&quot; data-ke-size=&quot;size16&quot;&gt;신규 PQ 시스템에서는 별도의 프로그램을 실행할 필요 없이 &lt;b&gt;기술인 경력증명서 PDF를 업로드하면 데이터를 자동으로 분석하고, 사용자가 추출된 내용을 확인한 뒤 기술인의 기본정보&amp;middot;자격&amp;middot;학력&amp;middot;교육&amp;middot;경력 데이터로 저장할 수 있도록 기능을 고도화할 예정이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3388&quot; data-start=&quot;3327&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3388&quot; data-start=&quot;3327&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로 이번 작은 자동화 도구가 신규 PQ 시스템의 정식 기능을 미리 검증하는 PoC 역할도 하게 되었다.&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;491&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKS4n9/dJMcabSUHCL/KpBbTwk3KV5SgjPsLEMhD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKS4n9/dJMcabSUHCL/KpBbTwk3KV5SgjPsLEMhD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKS4n9/dJMcabSUHCL/KpBbTwk3KV5SgjPsLEMhD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKS4n9%2FdJMcabSUHCL%2FKpBbTwk3KV5SgjPsLEMhD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;491&quot; height=&quot;203&quot; data-origin-width=&quot;491&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;  인터뷰를 통해 느낀 점&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개발자에게 최신 기술을 공부하고 잘 활용하는 능력도 중요하다고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 이번 현업 인터뷰와 자동화를 진행하면서 다시 한번 느낀 것은, &lt;/span&gt;&lt;b&gt;&lt;span&gt;어떤 기술을 사용했는가보다 어떤 문제를 해결했는가가 더 중요하다는 점&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음부터 거대한 시스템을 만들지 않더라도 현업에서 가장 많은 시간이 소요되는 작업을 찾아 작은 도구로 먼저 해결해보고, 실제 사용자의 피드백을 받아 정식 시스템으로 발전시키는 것도 좋은 개발 방법이라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞으로 신규 PQ 시스템을 개발하면서도 기존 기능을 그대로 구현하는 것에 그치지 않고, 현업에서 반복적으로 수행하고 있는 수작업을 찾아 하나씩 시스템화해보려고 한다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/[ERP] 풀스택 AI개발</category>
      <author>그린 파인애플</author>
      <guid isPermaLink="true">https://utlee0.tistory.com/8</guid>
      <comments>https://utlee0.tistory.com/8#entry8comment</comments>
      <pubDate>Wed, 26 Aug 2026 17:30:23 +0900</pubDate>
    </item>
  </channel>
</rss>