diff --git a/README.md b/README.md index 62cb0fa..30a7666 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,8 @@ publishDate: "2025-12-01" # optional, 예약 발행 `draft: true` 또는 미래의 `publishDate`가 있는 글은 production 빌드에서 자동으로 제외됩니다. `date`는 최초 게시일, `updated`는 최종 수정일이며 둘 다 필수입니다. 처음 게시할 때는 같은 값을 기록하고, 이후 본문·구조화 데이터·주요 링크처럼 검색 결과에 영향을 주는 내용을 실제로 수정했을 때만 `updated`를 변경합니다. 게시일보다 빠르거나 미래인 수정일은 빌드에서 거부됩니다. +관련 글처럼 자동 생성되는 주요 링크를 추가하거나 바꿀 때도 실제 HTML이 달라지는 글의 `updated`를 함께 기록합니다. 글의 태그·제목·설명을 수정하면 다른 글의 관련 링크도 달라질 수 있으므로 빌드 결과를 비교해 영향을 받은 글을 확인합니다. 변경이 없는 글의 날짜는 유지하며 빌드 날짜로 일괄 갱신하지 않습니다. + ## 프로젝트 구조 ```text diff --git a/content/posts/en/api-response-error-standardization.md b/content/posts/en/api-response-error-standardization.md index b804397..960d247 100644 --- a/content/posts/en/api-response-error-standardization.md +++ b/content/posts/en/api-response-error-standardization.md @@ -1,7 +1,7 @@ --- title: "Standardizing API Responses and Error Handling - Getting HTTP Status Codes Right" date: "2026-03-10" -updated: "2026-03-10" +updated: "2026-10-13" description: "How I migrated from a legacy structure where every API returned 200 to a shared response and error system that uses HTTP status codes properly, improving monitoring, debugging, and team communication." tags: ["java", "spring-boot", "architecture", "error-handling", "refactoring"] draft: false diff --git a/content/posts/en/auth-authorize-callback-flow.md b/content/posts/en/auth-authorize-callback-flow.md index 96bbb7e..014063b 100644 --- a/content/posts/en/auth-authorize-callback-flow.md +++ b/content/posts/en/auth-authorize-callback-flow.md @@ -1,7 +1,7 @@ --- title: "Splitting Login Responsibility (2) - Connecting the authorize/callback Flow" date: "2026-04-16" -updated: "2026-04-16" +updated: "2026-10-13" description: "How the frontend connects the login flow from protected page access to backend authorize, callback, and token issuance." tags: ["authentication", "oauth", "frontend", "backend", "typescript", "java", "spring"] series: "Splitting Login Responsibility" diff --git a/content/posts/en/backend-owned-login-flow.md b/content/posts/en/backend-owned-login-flow.md index 588c77d..fe25ee9 100644 --- a/content/posts/en/backend-owned-login-flow.md +++ b/content/posts/en/backend-owned-login-flow.md @@ -1,7 +1,7 @@ --- title: "Splitting Login Responsibility (1) - Why I Moved It from Frontend to Backend" date: "2026-04-15" -updated: "2026-04-15" +updated: "2026-10-13" description: "The responsibility boundaries I used while moving login state from a frontend-owned flow to a backend-owned authentication flow." tags: ["authentication", "oauth", "backend", "frontend", "architecture"] series: "Splitting Login Responsibility" diff --git a/content/posts/en/ecs-to-eks-migration.md b/content/posts/en/ecs-to-eks-migration.md index 421f2ed..ef69682 100644 --- a/content/posts/en/ecs-to-eks-migration.md +++ b/content/posts/en/ecs-to-eks-migration.md @@ -1,7 +1,7 @@ --- title: "From ECS to EKS - Building an Operating Environment with kustomize and Gateway API" date: "2026-03-12" -updated: "2026-03-12" +updated: "2026-10-13" description: "What I learned while migrating from AWS ECS to EKS and building an operating model with kustomize base/overlay, Kubernetes Gateway API, Karpenter node autoscaling, and blue/green deployment." tags: ["kubernetes", "eks", "kustomize", "gateway-api", "karpenter", "devops", "aws"] series: "Service Infrastructure Modernization" @@ -17,7 +17,7 @@ But as the number of services and environments grew, ECS started to show limits. ### Limits I Felt in ECS -The first pain was environment management. Dev, stage, and prod each had slightly different task definitions, environment variables, listener rules, health checks, and autoscaling settings. These differences were managed through Terraform and GitHub Actions, but the shape of each environment drifted over time. +The first pain was environment management. Dev, stage, and prod each had slightly different task definitions, environment variables, listener rules, health checks, and autoscaling settings. These differences lived in task definitions and GitHub Actions deployment scripts, making them harder to review together. The second pain was deployment control. ECS blue/green deployment works, but the operational model was not flexible enough for the workflows we wanted: @@ -31,9 +31,9 @@ The third pain was observability and platform extensibility. Once we started thi ### Why ECS Blue/Green Became Complicated -ECS blue/green deployment with CodeDeploy is convenient when the deployment shape is simple. But our deployment needed more control. +Our ECS blue/green deployment used two services and separate ALB target groups. The `deploy-blue.yml` and `deploy-green.yml` workflows queried listener rules to identify the active target group and deployed to the inactive service. -We wanted to compare two versions through shared routing rules, switch traffic explicitly, and keep the environment differences in version-controlled manifests. In ECS, a lot of this ended up split between task definitions, target groups, listener rules, CodeDeploy applications, and GitHub Actions scripts. +Traffic switching was a separate operation on the ALB listener rules. Build logic was duplicated across the two workflows, and the active slot was recorded in AWS state rather than Git. That split made the deployment hard to reason about. When a deployment failed, the question was not only "which version failed?" It was also "which AWS object currently owns the truth?" @@ -56,23 +56,30 @@ flowchart TB GH["GitHub Actions"] --> IMG["Build and push Docker image"] IMG --> REG["Container registry"] - GH -->|"dev/stage update"| KUS["kustomize overlay"] - KUS --> ARGO["ArgoCD"] - ARGO --> EKS["EKS cluster"] + REG --> UPDATER["Image Updater
dev/stage digest changes"] + UPDATER --> AUTO["ArgoCD auto-sync"] - subgraph cluster["EKS"] + GH -->|"prod manifest update"| KUS["kustomize prod overlay"] + KUS --> APPROVAL["n8n + Slack approval"] + APPROVAL --> SYNC["ArgoCD sync after approval"] + AUTO --> EKS + SYNC --> EKS + + subgraph EKS["EKS cluster"] GW["Gateway API + AWS Load Balancer Controller"] ROUTE["HTTPRoute"] - SVC["Kubernetes Service"] + BSVC["blue Service"] + GSVC["green Service"] BLUE["blue Deployment"] GREEN["green Deployment"] KAR["Karpenter"] + NODES["Workload nodes"] end - GW --> ROUTE --> SVC - SVC --> BLUE - SVC --> GREEN - KAR -->|"provision nodes"| EKS + GW --> ROUTE + ROUTE -->|"weight 0"| BSVC --> BLUE + ROUTE -->|"weight 100"| GSVC --> GREEN + KAR -->|"provision nodes"| NODES ``` The main pieces were: @@ -88,7 +95,7 @@ The main pieces were: ### Creating the EKS Cluster with eksctl -I used `eksctl` to create the base cluster. The cluster itself was intentionally kept small. Application capacity would be handled by Karpenter. +I used `eksctl` to create the base cluster. The system node group stayed small, while Karpenter supplied application capacity. The YAML excerpts below are abbreviated; names and hostnames are illustrative. ```yaml apiVersion: eksctl.io/v1alpha5 @@ -97,7 +104,7 @@ kind: ClusterConfig metadata: name: service-prod region: ap-northeast-2 - version: "1.31" + version: "1.34" iam: withOIDC: true @@ -107,7 +114,7 @@ managedNodeGroups: instanceTypes: ["t3.medium"] desiredCapacity: 2 minSize: 2 - maxSize: 4 + maxSize: 3 ``` The important part is `withOIDC: true`. Without OIDC, IRSA cannot be used, and Pods end up relying on node-level IAM permissions. @@ -186,11 +193,13 @@ spec: - name: app-api image: app-api:latest ports: - - containerPort: 8080 + - containerPort: 9285 readinessProbe: httpGet: - path: /actuator/health/readiness - port: 8080 + path: /status/health/readiness + port: 9285 + initialDelaySeconds: 40 + periodSeconds: 10 ``` ### Overlay - Patch Only the Differences @@ -203,17 +212,18 @@ kind: Deployment metadata: name: app-api spec: - replicas: 4 + replicas: 3 template: spec: containers: - name: app-api resources: requests: - cpu: "500m" - memory: "1Gi" + cpu: "100m" + memory: "2048Mi" limits: - memory: "2Gi" + cpu: "1500m" + memory: "4096Mi" env: - name: SPRING_PROFILES_ACTIVE value: prod @@ -225,7 +235,7 @@ spec: |------|-----|-------|------| | Deployment | automatic | automatic | Slack approval | | Replicas | small | similar to prod | production capacity | -| Image update | ArgoCD Image Updater | ArgoCD Image Updater | Git commit after approval | +| Image update | ArgoCD Image Updater | ArgoCD Image Updater | Git commit before approval; sync after approval | | Route | dev domain | stage domain | production domain | | Resource limit | relaxed | close to prod | strict | @@ -264,17 +274,19 @@ spec: apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: - name: public-gateway + name: common-gateway + namespace: networking spec: gatewayClassName: alb listeners: - name: https protocol: HTTPS port: 443 - hostname: "*.example.com" allowedRoutes: namespaces: from: All + tls: + mode: Terminate ``` ### HTTPRoute - Service Routing @@ -286,13 +298,15 @@ metadata: name: app-api spec: parentRefs: - - name: public-gateway + - name: common-gateway + namespace: networking + sectionName: https hostnames: - api.example.com rules: - backendRefs: - name: app-api - port: 80 + port: 9285 ``` ### TargetGroupConfiguration - Health Check @@ -300,7 +314,7 @@ spec: For AWS-specific health check settings, `TargetGroupConfiguration` was used. ```yaml -apiVersion: elbv2.k8s.aws/v1beta1 +apiVersion: gateway.k8s.aws/v1beta1 kind: TargetGroupConfiguration metadata: name: app-api @@ -309,9 +323,12 @@ spec: group: "" kind: Service name: app-api - healthCheckConfig: - path: /actuator/health/readiness - port: "8080" + defaultConfiguration: + targetType: ip + healthCheckConfig: + healthCheckPath: /status/health/liveness + healthCheckPort: "9285" + healthCheckProtocol: HTTP ``` ## Karpenter - Node Autoscaling @@ -328,55 +345,59 @@ Karpenter can look at pending pods, choose a fitting instance type, and provisio apiVersion: karpenter.sh/v1 kind: NodePool metadata: - name: app + name: nodepool spec: template: spec: requirements: - key: kubernetes.io/arch operator: In - values: ["amd64"] - - key: karpenter.sh/capacity-type - operator: In - values: ["on-demand"] - - key: node.kubernetes.io/instance-type - operator: In - values: ["m6i.large", "m6i.xlarge", "m7i.large", "m7i.xlarge"] + values: ["arm64"] disruption: consolidationPolicy: WhenEmptyOrUnderutilized + consolidateAfter: 30s ``` -The design was intentionally conservative at first. Production used on-demand capacity, and instance families were limited to a known set. Spot capacity can be added later for workloads that tolerate interruption. +Workload NodePools used ARM64 and were separated for dev, stage, production backend, and production web workloads. Taints and tolerations kept those workloads in their intended pools. CI built multi-architecture images with `--platform linux/amd64,linux/arm64` so the same image reference could run on either architecture. ## Blue/Green Deployment Strategy ### Structure -Blue/green deployment was expressed with labels and services. +The frontend (`my-web`) used two independent Deployments, each with its own Service and HPA. An HTTPRoute referred to both Services and controlled which slot received traffic. ```mermaid flowchart LR - R["HTTPRoute"] --> S["Service selector: color=blue"] - S --> B["Deployment app-api-blue"] - G["Deployment app-api-green"] + R["HTTPRoute"] -->|"weight 100"| GS["Service my-web-green"] + GS --> G["Deployment my-web-green"] + R -->|"weight 0"| BS["Service my-web-blue"] + BS --> B["Deployment my-web-blue"] ``` -The inactive color can be deployed, warmed up, and checked before traffic is switched. Traffic switching is done by changing the service selector or route target. +The inactive slot could be deployed and checked while the active slot continued serving requests. Traffic switching changed the HTTPRoute weights between `100` and `0`; the Services kept their existing selectors. ### Traffic Switching ```yaml -apiVersion: v1 -kind: Service +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute metadata: - name: app-api + name: my-web-route spec: - selector: - app: app-api - color: green + hostnames: + - app.example.io + - app.example.com + rules: + - backendRefs: + - name: my-web-green + port: 3000 + weight: 100 + - name: my-web-blue + port: 3000 + weight: 0 ``` -This model made rollback simple. If green fails, switch the selector back to blue. The switch is small, visible, and version-controlled. +With green active in this example, the next image was deployed to blue first. After its health checks passed, blue received weight `100` and green received weight `0`. The previous slot stayed running, so rollback restored the previous weights without rebuilding or redeploying the old version. The route change was visible in Git. ### HPA - Traffic-Based Scaling @@ -386,21 +407,21 @@ HPA handles pod count, and Karpenter handles node count. The two are separate bu apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: - name: app-api + name: my-web-blue spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment - name: app-api - minReplicas: 2 - maxReplicas: 20 + name: my-web-blue + minReplicas: 6 + maxReplicas: 24 metrics: - type: Resource resource: name: cpu target: type: Utilization - averageUtilization: 60 + averageUtilization: 30 ``` If traffic increases, HPA creates more pods. If there is no capacity, pods become pending. Karpenter sees the pending pods and provisions new nodes. @@ -412,8 +433,8 @@ If traffic increases, HPA creates more pods. If there is no capacity, pods becom For dev and stage, deployment is fully automatic. 1. GitHub Actions builds and pushes an image. -2. ArgoCD Image Updater detects the new tag. -3. The kustomize overlay is updated. +2. ArgoCD Image Updater detects a changed digest for the mutable tag. +3. Image Updater updates ArgoCD's image override when the digest changes. 4. ArgoCD syncs the cluster. This gives fast feedback without manual approval. @@ -423,11 +444,11 @@ This gives fast feedback without manual approval. Production is different. 1. GitHub Actions builds the image. -2. n8n sends a Slack approval message. -3. After approval, a Git commit updates the prod overlay image tag. -4. ArgoCD applies the new desired state. +2. A Git commit updates the prod overlay with the immutable image tag and records the previous tag for rollback. +3. n8n sends a Slack approval message. +4. After approval, ArgoCD syncs the committed desired state. -Production deployment still becomes GitOps: the cluster changes only after Git changes. +Production deployment uses the committed manifest as its desired state, with approval gating the ArgoCD sync. ### Before/After Comparison @@ -438,7 +459,7 @@ Production deployment still becomes GitOps: the cluster changes only after Git c | Ingress | ALB listener rules | Gateway API | | Autoscaling | ECS service + ASG | HPA + Karpenter | | Deployment source | CI scripts and AWS state | Git manifests | -| Rollback | CodeDeploy or AWS console | Git state and route switch | +| Rollback | Previous task definition and ALB rules | Previous image tag or HTTPRoute weights | ## Things That Went Wrong @@ -446,7 +467,7 @@ Production deployment still becomes GitOps: the cluster changes only after Git c The application needed time to warm up. When the readiness probe started too early, the deployment looked unhealthy even though the app would have become ready a few seconds later. -The fix was to tune `initialDelaySeconds`, `periodSeconds`, and the Spring Boot readiness endpoint so that Kubernetes only routed traffic after the application was actually ready. +The readiness delay was increased from `20` to `40` seconds, the check period from `5` to `10` seconds, and `failureThreshold` was set to `5`. This gave the Spring Boot application time to become ready before receiving traffic. ### Gateway API allowedRoutes @@ -454,9 +475,7 @@ At first, `HTTPRoute` resources in other namespaces did not attach to the shared ### Karpenter Consolidation and PDB Conflict -Karpenter consolidation tries to remove or replace underused nodes. PodDisruptionBudget can block that. If PDB settings are too strict, consolidation does not happen and node cost stays high. - -The fix was not to remove PDB, but to set realistic disruption budgets for each workload. +In this migration, production workloads were briefly disrupted during consolidation because a PodDisruptionBudget had not been configured. A PDB with `minAvailable: 1` was added to preserve a minimum number of available Pods during voluntary disruptions. ### LoadRestrictionsNone diff --git a/content/posts/en/gitops-argocd-n8n-deployment.md b/content/posts/en/gitops-argocd-n8n-deployment.md index 6bce7bd..daa4de4 100644 --- a/content/posts/en/gitops-argocd-n8n-deployment.md +++ b/content/posts/en/gitops-argocd-n8n-deployment.md @@ -1,7 +1,7 @@ --- title: "GitOps Deployment Automation - Building a Slack Approval Pipeline with ArgoCD, n8n, and GitHub Actions" date: "2026-02-26" -updated: "2026-02-26" +updated: "2026-10-13" description: "How I built a GitOps-based deployment automation system during the ECS to EKS migration, combining ArgoCD, n8n, and GitHub Actions with different deployment strategies per environment." tags: ["kubernetes", "argocd", "gitops", "n8n", "devops", "github-actions"] draft: false @@ -28,16 +28,16 @@ flowchart TB CI --> IMG["Build image"] IMG --> REG["Push image to registry"] - REG --> UPDATER["ArgoCD Image Updater
(dev/stage)"] - UPDATER --> GIT1["Commit image tag to overlay"] - GIT1 --> ARGO["ArgoCD sync"] + REG --> UPDATER["ArgoCD Image Updater
Detect mutable-tag digest changes
(dev/stage)"] + UPDATER --> ARGO["ArgoCD auto-sync
(dev/stage)"] - CI --> N8N["n8n workflow
(prod)"] + CI --> GIT2["Commit prod image tag"] + GIT2 --> N8N["n8n workflow
(prod)"] N8N --> SLACK["Slack approval"] - SLACK --> GIT2["Commit prod image tag"] - GIT2 --> ARGO + SLACK --> SYNC["n8n calls ArgoCD Sync API"] ARGO --> EKS["EKS cluster"] + SYNC --> EKS ``` The important distinction is that no deployment tool directly edits Kubernetes production objects. It edits Git, and ArgoCD applies the desired state. @@ -47,10 +47,9 @@ The important distinction is that no deployment tool directly edits Kubernetes p For dev and stage, the goal is fast feedback. 1. GitHub Actions builds the image. -2. The image is pushed to the registry. -3. ArgoCD Image Updater detects the new image. -4. It updates the kustomize overlay. -5. ArgoCD syncs the application. +2. The image is pushed to the registry with the mutable `dev` or `stage` tag. +3. ArgoCD Image Updater detects a change in the image digest, even when the tag name stays the same. +4. ArgoCD automatically syncs the application. There is no manual approval. If something breaks, the environment is not customer-facing and can be fixed quickly. @@ -58,13 +57,13 @@ There is no manual approval. If something breaks, the environment is not custome Production has a different policy. -1. GitHub Actions builds the image. -2. n8n receives release metadata. -3. n8n sends a Slack message with approve/reject buttons. -4. If approved, n8n creates a Git commit that updates the prod overlay image tag. -5. ArgoCD syncs the new desired state. +1. GitHub Actions builds and pushes the image with an immutable tag. +2. It commits that tag to the prod overlay in the manifest repository. +3. n8n receives release metadata and sends a Slack message with approve/reject buttons. +4. If approved, n8n calls the ArgoCD Sync API. +5. ArgoCD applies the new desired state. -This keeps production deployment auditable. The approval happens in Slack, but the actual deployment state is still Git. +The manifest commit happens before approval, so prod auto-sync stays disabled. The approval happens in Slack, and n8n explicitly starts the sync. Git still records the desired deployment state. ## Core Components @@ -90,12 +89,14 @@ jobs: run: docker push app-api:${{ github.sha }} ``` -The CI job does not apply Kubernetes manifests. It only produces an immutable image and metadata. +The snippet shows the image build and push. In the prod workflow, CI then commits the image tag to the manifest repository and calls the n8n webhook to request approval. It does not directly apply Kubernetes objects. ### 2. ArgoCD - Cluster State Management ArgoCD watches the Git repository and makes the cluster match it. +In dev and stage, Image Updater detects digest changes under the mutable tags and triggers automatic sync; changing the tag name is not required. + ```yaml apiVersion: argoproj.io/v1alpha1 kind: Application @@ -108,13 +109,10 @@ spec: destination: server: https://kubernetes.default.svc namespace: app-prod - syncPolicy: - automated: - prune: false - selfHeal: true + syncPolicy: {} ``` -For prod, auto-sync can still be used because approval happens before Git changes. The Git commit itself is the gate. +The prod Application has no automated sync policy. Updating Git does not start a production rollout; n8n calls the Sync API only after Slack approval. Dev and stage use automatic sync instead. ### 3. n8n - Slack Approval Workflow @@ -125,11 +123,12 @@ flowchart LR A["GitHub Actions webhook"] --> B["n8n"] B --> C["Create Slack approval message"] C --> D{"Approved?"} - D -->|"Yes"| E["Commit image tag to prod overlay"] + D -->|"Yes"| E["Call ArgoCD Sync API"] + E --> G["Notify deployment result"] D -->|"No"| F["Stop workflow"] ``` -n8n was useful because the workflow is mostly integration logic: receive metadata, format Slack messages, wait for interaction, call GitHub API, and notify the result. +n8n was useful because the workflow is mostly integration logic: receive metadata, format Slack messages, wait for interaction, call the ArgoCD Sync API, and notify the result. ### 4. kustomize - Environment Separation @@ -144,15 +143,15 @@ apps/app-api/ prod/ ``` -The production approval workflow only changes the image tag in the prod overlay. It does not touch dev or stage. +CI updates the image tag in the prod overlay before requesting approval. The approval workflow then syncs the prod Application. It does not deploy dev or stage. ## Environment Strategy Comparison -| Environment | Deployment trigger | Approval | Rollback | -|-------------|--------------------|----------|----------| -| dev | merge or image build | none | update overlay | -| stage | merge or image build | none | update overlay | -| prod | release workflow | Slack approval | revert Git commit | +| Environment | Deployment trigger | Approval | ArgoCD sync | Rollback | +|-------------|--------------------|----------|-------------|----------| +| dev | merge or image build | none | automatic | re-push previous image (same tag) | +| stage | merge or image build | none | automatic | re-push previous image (same tag) | +| prod | manifest update, then Slack approval | Slack approval | explicit Sync API call after approval | revert Git commit, then sync | The key is that all environments share the same manifest structure, but not the same policy. @@ -160,9 +159,9 @@ The key is that all environments share the same manifest structure, but not the ### Why I Delayed ArgoCD for prod at First -At first, I considered applying ArgoCD only to dev and stage. Production felt risky because auto-sync sounded like "automatic production deployment." +At first, I planned to bring ArgoCD into production immediately. Rollback problems during deployment showed that dev and stage needed more verification first. I stabilized those environments while building the n8n approval gate, then brought the flow to production more cautiously. -The important realization was that auto-sync and auto-release are different. ArgoCD auto-sync only applies Git state. If Git changes are controlled by Slack approval, production is still gated. +Production kept automatic sync disabled. A manifest commit prepared the change, and Slack approval allowed n8n to start the sync. ### Why stage Became Fully Automatic @@ -193,6 +192,6 @@ The fix was to align Kubernetes readiness timing with Spring Boot startup behavi GitOps is not just "use ArgoCD." The important question is where the source of truth lives. -If CI directly changes the cluster, the source of truth is split between scripts, cluster state, and the repository. If CI only builds artifacts and deployment changes happen through Git, the model becomes much easier to reason about. +If CI directly changes the cluster, the source of truth is split between scripts, cluster state, and the repository. Recording deployment changes in Git and controlling when ArgoCD applies them makes the model easier to reason about. -The final structure was simple: GitHub Actions builds, n8n approves, Git records, and ArgoCD applies. +The final structure was simple: GitHub Actions builds and records the manifest change in Git, n8n handles Slack approval and starts the sync, and ArgoCD applies it. diff --git a/content/posts/en/jdk-migration-strategy-11-to-25.md b/content/posts/en/jdk-migration-strategy-11-to-25.md index f8f0b85..be137a9 100644 --- a/content/posts/en/jdk-migration-strategy-11-to-25.md +++ b/content/posts/en/jdk-migration-strategy-11-to-25.md @@ -1,8 +1,8 @@ --- title: "JDK Migration (1) - From JDK 11 to 21 and the Spring Boot 3.x Transition" date: "2026-03-18" -updated: "2026-03-18" -description: "A practical record of upgrading a production Spring Boot service from JDK 11 to 21, including the javax to jakarta transition, AWS SDK migration, and QueryDSL compatibility work." +updated: "2026-10-13" +description: "A practical record of upgrading a production Spring Boot service from JDK 11 to 21, including the javax to jakarta transition, AWS SDK for Java 1.x to 2.x migration, Spring Cloud AWS 3.x, and QueryDSL compatibility work." tags: ["java", "jdk", "spring-boot", "migration", "devops"] series: "JDK Migration" seriesOrder: 1 @@ -33,7 +33,8 @@ The migration touched many files because the change crossed framework boundaries | Spring Boot | 2.x -> 3.x | | Java EE namespace | `javax.*` -> `jakarta.*` | | QueryDSL | Jakarta-compatible classifier | -| AWS integration | old SDK/Spring Cloud AWS setup -> newer awspring style | +| AWS SDK for Java | 1.x -> 2.x | +| Spring Cloud AWS | 2.4.4 -> 3.3.0 | | MySQL connector | old driver naming and dependency update | | Spring Security | API and configuration updates | @@ -89,23 +90,27 @@ annotationProcessor "jakarta.annotation:jakarta.annotation-api" Without the `jakarta` classifier, generated Q classes may still depend on `javax.persistence`, which creates confusing compile errors. -## AWS SDK v1 to v3 through awspring cloud +## AWS SDK for Java 1.x to 2.x with Spring Cloud AWS 3.x + +These are two separate version changes: AWS SDK for Java moved from 1.x to 2.x, while Spring Cloud AWS moved from 2.4.4 to 3.3.0. The [Spring Cloud AWS compatibility table](https://awspring.io/what-is-spring-cloud-aws#compatibility-with-spring-project-versions) identifies AWS SDK for Java 2.x as the SDK used by Spring Cloud AWS 3.3.x. ### Dependency Changes -AWS integration also changed. The old setup mixed AWS SDK versions and Spring Cloud AWS APIs. The migration moved toward the newer awspring ecosystem. +The old AWS SDK for Java dependencies used the `com.amazonaws` group. SDK 2.x uses `software.amazon.awssdk`, as described in the [official migration guide](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/migration-whats-different.html). The integration library's `3.3.0` version and the Lambda client's `2.30.27` version below belong to different version lines. ```gradle -implementation "io.awspring.cloud:spring-cloud-aws-starter-sqs" -implementation "io.awspring.cloud:spring-cloud-aws-starter-s3" -implementation "software.amazon.awssdk:s3" +implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.3.0") +implementation "io.awspring.cloud:spring-cloud-aws-starter:3.3.0" +implementation "io.awspring.cloud:spring-cloud-aws-sqs:3.3.0" +implementation "io.awspring.cloud:spring-cloud-aws-s3:3.3.0" +implementation "software.amazon.awssdk:lambda:2.30.27" ``` The goal was to make AWS clients explicit and compatible with Spring Boot 3.x. ### Configuration Structure -The configuration also changed from older property names to the awspring style. +The configuration prefix changed from `cloud.aws.*` to `spring.cloud.aws.*`. ```yaml spring: @@ -118,23 +123,25 @@ spring: name: default ``` -In production, credentials are not configured as static keys. They are resolved through IAM roles, usually IRSA on EKS. +For an EKS deployment using IRSA, credentials can be resolved through the workload's IAM role. The profile setting above is a configuration example rather than an account of production credential storage. ### Service Code Migration -Some service code changed from older clients to SDK v2-style clients. +SDK 2.x provides both synchronous `S3Client` and asynchronous `S3AsyncClient` APIs; asynchronous operation is a client choice, not the default behavior of every SDK call. The [AWS S3 client guide](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/examples-s3.html) explains the available clients. The upload example from this migration changed from `AmazonS3` to `S3AsyncClient`: ```java -PutObjectRequest request = PutObjectRequest.builder() - .bucket(bucket) - .key(key) - .contentType(contentType) - .build(); - -s3Client.putObject(request, RequestBody.fromBytes(bytes)); +// Before: AWS SDK for Java 1.x, synchronous upload +AmazonS3 legacyClient; +legacyClient.putObject(bucket, key, file); + +// After: AWS SDK for Java 2.x, asynchronous upload +S3AsyncClient s3Client; +CompletableFuture upload = s3Client.putObject( + PutObjectRequest.builder().bucket(bucket).key(key).build(), + AsyncRequestBody.fromFile(file)); ``` -The important point is not the syntax itself. The important point is that AWS client construction, region, and credentials become explicit and testable. +An asynchronous upload returns a `CompletableFuture`, so the caller must handle completion and failure. Client construction, region, and credential resolution also need to be checked independently of the API rename. ## MySQL Connector Changes @@ -186,9 +193,7 @@ The fix was to use Jakarta-compatible QueryDSL dependencies consistently for bot ### AWS SDK Async Migration -Some AWS libraries encourage async clients. Async clients are useful, but they also change execution and error handling. I avoided mixing async conversion into the same step unless the calling code actually needed it. - -The migration rule was: first make it compatible, then optimize. +An async client changes completion and error handling at the call site. A synchronous path can use `S3Client`; a path using `S3AsyncClient` needs to account for its returned future. Calling `.join()` waits on the calling thread, so replacing a synchronous call with an async client and immediately joining it does not make that caller non-blocking. ### Changing 219 Files at Once diff --git a/content/posts/en/jdk-migration-strategy-21-to-25.md b/content/posts/en/jdk-migration-strategy-21-to-25.md index f4bc947..b25f6c3 100644 --- a/content/posts/en/jdk-migration-strategy-21-to-25.md +++ b/content/posts/en/jdk-migration-strategy-21-to-25.md @@ -1,7 +1,7 @@ --- title: "JDK Migration (2) - From JDK 21 to 25, Virtual Threads, and Modern Java" date: "2026-03-18" -updated: "2026-03-18" +updated: "2026-10-13" description: "A practical record of upgrading from JDK 21 to 25, enabling Virtual Threads, handling pinning issues, applying pattern matching switch, and moving to Gradle Kotlin DSL." tags: ["java", "jdk", "spring-boot", "virtual-threads", "devops"] series: "JDK Migration" diff --git a/content/posts/en/jwt-hs256-to-rs256-jwks-kms.md b/content/posts/en/jwt-hs256-to-rs256-jwks-kms.md index afc7cda..051c609 100644 --- a/content/posts/en/jwt-hs256-to-rs256-jwks-kms.md +++ b/content/posts/en/jwt-hs256-to-rs256-jwks-kms.md @@ -1,7 +1,7 @@ --- title: "JWT Signing Migration - From HS256 to RS256/JWKS/KMS" date: "2026-04-24" -updated: "2026-04-24" +updated: "2026-10-13" description: "How I split signing responsibility, kid, key rotation, and legacy token handling while moving symmetric JWT signing to RS256, JWKS, and KMS." tags: ["authentication", "jwt", "jwks", "kms", "java", "spring", "security"] draft: false diff --git a/content/posts/en/lgtm-stack-observability.md b/content/posts/en/lgtm-stack-observability.md index 9d40bd0..bd69e66 100644 --- a/content/posts/en/lgtm-stack-observability.md +++ b/content/posts/en/lgtm-stack-observability.md @@ -1,7 +1,7 @@ --- title: "Replacing Datadog with an LGTM Stack - OTel Sidecar with Grafana, Loki, Tempo, and Mimir" date: "2026-03-17" -updated: "2026-03-17" +updated: "2026-10-13" description: "How I moved from Datadog to an open-source LGTM stack and built observability with an OTel Collector sidecar, noise filtering, MDC request tracing, and IRSA-backed S3 storage." tags: ["grafana", "loki", "tempo", "mimir", "opentelemetry", "kubernetes", "observability", "devops"] series: "Service Infrastructure Modernization" diff --git a/content/posts/en/lgtm-stack-setup-guide.md b/content/posts/en/lgtm-stack-setup-guide.md index 3ee6032..b98f4be 100644 --- a/content/posts/en/lgtm-stack-setup-guide.md +++ b/content/posts/en/lgtm-stack-setup-guide.md @@ -1,7 +1,7 @@ --- title: "Building an LGTM Stack on EKS - Practical Setup Guide for Loki, Grafana, Tempo, and Mimir" date: "2026-03-17" -updated: "2026-03-17" +updated: "2026-10-13" description: "A practical guide to deploying Grafana's LGTM stack on EKS with Helm and collecting Spring Boot logs, traces, and metrics through an OTel Collector sidecar." tags: ["grafana", "loki", "tempo", "mimir", "opentelemetry", "kubernetes", "helm", "observability"] draft: false diff --git a/content/posts/en/multi-channel-notification-server.md b/content/posts/en/multi-channel-notification-server.md index 1a47c80..927ad91 100644 --- a/content/posts/en/multi-channel-notification-server.md +++ b/content/posts/en/multi-channel-notification-server.md @@ -1,7 +1,7 @@ --- title: "Building a Multi-Channel Notification Server - From PHP Legacy to Java/Spring" date: "2026-03-09" -updated: "2026-03-09" +updated: "2026-10-13" description: "How I moved scattered PHP notification logic into an independent Spring Boot notification server that handles Kakao AlimTalk, SMS, email, push, and Slack through one structure." tags: ["java", "spring-boot", "notification", "sqs", "architecture"] draft: false diff --git a/content/posts/en/payment-system-redesign-sync-api.md b/content/posts/en/payment-system-redesign-sync-api.md index e6e7f26..cda318f 100644 --- a/content/posts/en/payment-system-redesign-sync-api.md +++ b/content/posts/en/payment-system-redesign-sync-api.md @@ -1,7 +1,7 @@ --- title: "Payment System Redesign (2) - Removing SQS and Moving to Synchronous APIs" date: "2026-03-06" -updated: "2026-03-06" +updated: "2026-10-13" description: "SQS looked attractive as an event-driven architecture, but in the payment domain it added more complexity than value. This post covers how I removed SQS and stabilized payment processing with synchronous APIs and Portone webhooks." tags: ["java", "spring-boot", "payment", "portone", "refactoring"] series: "Payment System Redesign" diff --git a/content/posts/en/spring-ai-cs-automation.md b/content/posts/en/spring-ai-cs-automation.md index 2596f3d..9b53aa6 100644 --- a/content/posts/en/spring-ai-cs-automation.md +++ b/content/posts/en/spring-ai-cs-automation.md @@ -1,7 +1,7 @@ --- title: "Operating a CS Support System with Spring AI - From Function Calling to an Operator Suggestion Workflow" date: "2026-07-06" -updated: "2026-07-28" +updated: "2026-10-13" description: "How a Spring AI customer-support draft system moved from function calling to parallel lookups, QA, error diagnosis, and an operator-review workflow in production." tags: ["spring-ai", "spring-boot", "ai", "llm", "cs"] draft: false diff --git a/content/posts/en/spring-ai-guide-01-setup.md b/content/posts/en/spring-ai-guide-01-setup.md index 8b9207e..70bf3bd 100644 --- a/content/posts/en/spring-ai-guide-01-setup.md +++ b/content/posts/en/spring-ai-guide-01-setup.md @@ -1,7 +1,7 @@ --- title: "Spring AI Guide (1) - From Project Setup to the First AI Call" date: "2026-03-17" -updated: "2026-03-17" +updated: "2026-10-13" description: "A guide to Spring AI core concepts, project setup, and the first AI call with ChatClient." tags: ["spring-ai", "spring-boot", "ai", "llm", "java"] series: "Spring AI Guide" diff --git a/content/posts/en/spring-ai-guide-02-multi-provider.md b/content/posts/en/spring-ai-guide-02-multi-provider.md index d0d2078..e0dc769 100644 --- a/content/posts/en/spring-ai-guide-02-multi-provider.md +++ b/content/posts/en/spring-ai-guide-02-multi-provider.md @@ -1,7 +1,7 @@ --- title: "Spring AI Guide (2) - Multi-Provider Strategy" date: "2026-03-17" -updated: "2026-03-17" +updated: "2026-10-13" description: "How to abstract OpenAI, AWS Bedrock, and Google Gemini behind one ChatClient interface and switch providers dynamically at runtime." tags: ["spring-ai", "spring-boot", "ai", "openai", "bedrock", "gemini"] series: "Spring AI Guide" diff --git a/content/posts/en/spring-ai-guide-03-prompt-structured-output.md b/content/posts/en/spring-ai-guide-03-prompt-structured-output.md index 2fd4f87..98f706e 100644 --- a/content/posts/en/spring-ai-guide-03-prompt-structured-output.md +++ b/content/posts/en/spring-ai-guide-03-prompt-structured-output.md @@ -1,7 +1,7 @@ --- title: "Spring AI Guide (3) - Prompt Management and Structured Output" date: "2026-03-17" -updated: "2026-03-17" +updated: "2026-10-13" description: "How to use Spring AI's message model, variable templating, database-backed prompt management, and JSON Schema-based structured output." tags: ["spring-ai", "spring-boot", "ai", "prompt-engineering", "structured-output"] series: "Spring AI Guide" diff --git a/content/posts/en/spring-ai-pipeline-real-world.md b/content/posts/en/spring-ai-pipeline-real-world.md index 1bf56f7..7f3d5fb 100644 --- a/content/posts/en/spring-ai-pipeline-real-world.md +++ b/content/posts/en/spring-ai-pipeline-real-world.md @@ -1,7 +1,7 @@ --- title: "Spring AI in Practice - Building a Seven-Step AI Diagnostic Pipeline" date: "2026-02-28" -updated: "2026-02-28" +updated: "2026-10-13" description: "A real-world record of redesigning direct HTTP calls and one large synchronous prompt into a seven-step Spring AI pipeline." tags: ["spring-ai", "spring-boot", "ai", "llm"] draft: false diff --git a/content/posts/en/spring-facade-pattern-layered-architecture.md b/content/posts/en/spring-facade-pattern-layered-architecture.md index 6d2f16b..7483802 100644 --- a/content/posts/en/spring-facade-pattern-layered-architecture.md +++ b/content/posts/en/spring-facade-pattern-layered-architecture.md @@ -1,7 +1,7 @@ --- title: "Introducing the Facade Pattern to Spring MVC - Improving Layered Architecture with a @Gateway Annotation" date: "2026-03-10" -updated: "2026-03-10" +updated: "2026-10-13" description: "How I introduced a custom @Gateway annotation and facade layer into a traditional Controller-Service-Repository architecture to clarify domain responsibilities and improve team productivity." tags: ["java", "spring-boot", "architecture", "design-pattern", "facade"] draft: false diff --git a/content/posts/en/sync-virtual-coroutine-benchmark.md b/content/posts/en/sync-virtual-coroutine-benchmark.md index 93fbef6..26b49b7 100644 --- a/content/posts/en/sync-virtual-coroutine-benchmark.md +++ b/content/posts/en/sync-virtual-coroutine-benchmark.md @@ -1,7 +1,7 @@ --- title: "Platform Thread vs Virtual Thread vs Coroutine - 10,000 Task Benchmark" date: "2026-03-24" -updated: "2026-03-24" +updated: "2026-10-13" description: "A benchmark comparing JDK 25 Platform Threads, Virtual Threads, and Kotlin Coroutines across I/O-bound, CPU-bound, and high-concurrency scenarios with 10,000 tasks repeated 100 times." tags: ["java", "kotlin", "virtual-threads", "coroutine", "benchmark", "concurrency"] draft: false diff --git a/content/posts/ko/api-response-error-standardization.md b/content/posts/ko/api-response-error-standardization.md index 9ea077c..0f337d4 100644 --- a/content/posts/ko/api-response-error-standardization.md +++ b/content/posts/ko/api-response-error-standardization.md @@ -1,7 +1,7 @@ --- title: "API 응답/에러 처리 구조 공통화 - HTTP 상태코드를 제대로 쓰기까지" date: "2026-03-10" -updated: "2026-03-10" +updated: "2026-10-13" description: "모든 API가 200을 반환하던 레거시 구조에서, HTTP 상태코드를 올바르게 활용하는 공통 응답/에러 체계로 전면 개선한 과정. 모니터링 정상화, 디버깅 효율화, 개발자 간 커뮤니케이션 비용 절감까지." tags: ["java", "spring-boot", "architecture", "error-handling", "refactoring"] draft: false diff --git a/content/posts/ko/auth-authorize-callback-flow.md b/content/posts/ko/auth-authorize-callback-flow.md index ce7dd36..333a655 100644 --- a/content/posts/ko/auth-authorize-callback-flow.md +++ b/content/posts/ko/auth-authorize-callback-flow.md @@ -1,7 +1,7 @@ --- title: "로그인 책임 분리 (2) - authorize/callback 흐름 연결하기" date: "2026-04-16" -updated: "2026-04-16" +updated: "2026-10-13" description: "보호 페이지 접근부터 백엔드 authorize, callback, 토큰 발급까지 이어지는 로그인 흐름을 프론트엔드에서 어떻게 연결했는지 정리합니다." tags: ["authentication", "oauth", "frontend", "backend", "typescript", "java", "spring"] series: "로그인 책임 분리" diff --git a/content/posts/ko/aws-to-gcp-migration.md b/content/posts/ko/aws-to-gcp-migration.md index 16bd1e0..575b477 100644 --- a/content/posts/ko/aws-to-gcp-migration.md +++ b/content/posts/ko/aws-to-gcp-migration.md @@ -1,7 +1,7 @@ --- title: "AWS에서 GCP로 - 클라우드 이관에서 실제로 바뀐 것들" date: "2026-06-18" -updated: "2026-06-18" +updated: "2026-10-13" description: "AWS에서 GCP로 서비스를 옮기면서 런타임, 배포, 시크릿, 데이터 연결, 캐시, 관측성의 기준이 어떻게 바뀌었는지 정리합니다." tags: ["gcp", "aws", "migration", "kubernetes", "devops", "cloud"] series: "서비스 인프라 현대화" diff --git a/content/posts/ko/backend-owned-login-flow.md b/content/posts/ko/backend-owned-login-flow.md index 4da3f1c..3885952 100644 --- a/content/posts/ko/backend-owned-login-flow.md +++ b/content/posts/ko/backend-owned-login-flow.md @@ -1,7 +1,7 @@ --- title: "로그인 책임 분리 (1) - 프론트엔드에서 백엔드로 옮긴 이유" date: "2026-04-15" -updated: "2026-04-15" +updated: "2026-10-13" description: "프론트엔드가 로그인 상태를 직접 만들던 구조를 백엔드 중심의 인증 흐름으로 바꾸며 정리한 책임 분리 기준." tags: ["authentication", "oauth", "backend", "frontend", "architecture"] series: "로그인 책임 분리" diff --git a/content/posts/ko/ecs-to-eks-migration.md b/content/posts/ko/ecs-to-eks-migration.md index b4e9ff7..7179a0b 100644 --- a/content/posts/ko/ecs-to-eks-migration.md +++ b/content/posts/ko/ecs-to-eks-migration.md @@ -1,7 +1,7 @@ --- title: "ECS에서 EKS로 - kustomize + Gateway API 기반 운영 환경 구축기" date: "2026-03-12" -updated: "2026-03-12" +updated: "2026-10-13" description: "AWS ECS에서 EKS로 마이그레이션하면서 kustomize base/overlay 패턴, K8s Gateway API, Karpenter 노드 오토스케일링, Blue/Green 배포 전략까지 구축한 경험을 공유합니다." tags: ["kubernetes", "eks", "kustomize", "gateway-api", "karpenter", "devops", "aws"] series: "서비스 인프라 현대화" diff --git a/content/posts/ko/gitops-argocd-n8n-deployment.md b/content/posts/ko/gitops-argocd-n8n-deployment.md index b5bf9c0..7d076a1 100644 --- a/content/posts/ko/gitops-argocd-n8n-deployment.md +++ b/content/posts/ko/gitops-argocd-n8n-deployment.md @@ -1,7 +1,7 @@ --- title: "GitOps 자동화 배포 - ArgoCD + n8n + GitHub Actions로 Slack 승인 배포 파이프라인 구축하기" date: "2026-02-26" -updated: "2026-02-26" +updated: "2026-10-13" description: "ECS에서 EKS로 마이그레이션하면서 구축한 GitOps 기반 배포 자동화 시스템을 소개합니다. ArgoCD, n8n, GitHub Actions를 조합하여 환경별로 차등화된 배포 전략을 설계한 경험을 공유합니다." tags: ["kubernetes", "argocd", "gitops", "n8n", "devops", "github-actions"] draft: false @@ -46,8 +46,6 @@ flowchart TB end ``` -> **Screenshot 제안**: ArgoCD 대시보드에서 Application 목록이 보이는 화면 (dev/stage/prod 환경이 함께 보이면 좋음) - ### dev / stage - 완전 자동 개발 환경은 속도가 중요하다. 브랜치에 push하면 알아서 배포된다. 별도 승인 같은 건 없다. @@ -120,8 +118,6 @@ spec: dev/stage에서는 ArgoCD Image Updater가 ECR에 올라간 이미지의 digest 변경을 감지해서 자동으로 sync한다. mutable tag라 태그 이름은 같지만 실제 내용물(digest)이 바뀌면 배포가 트리거된다. -> **Screenshot 제안**: ArgoCD Application 상세 화면 - Sync 상태(Synced/OutOfSync)와 Health 상태가 보이는 화면 - ### 3. n8n - Slack 승인 워크플로우 n8n은 prod 배포에서 GitHub Actions와 ArgoCD 사이의 **승인 게이트** 역할을 한다. 오픈소스 워크플로우 자동화 도구인데, 이런 연결 작업에 딱이다. @@ -148,10 +144,6 @@ n8n을 선택한 이유가 몇 가지 있다: - **셀프호스팅** - EKS 클러스터 안에서 돌리고 있어서 외부 의존성이 없다 - **재사용성** - 환경 변수만 바꾸면 같은 워크플로우를 여러 서비스에 적용할 수 있다 -> **Screenshot 제안**: n8n 워크플로우 에디터 화면 - Webhook → Slack 메시지 → 승인 대기 → ArgoCD Sync 노드들이 연결된 모습 - -> **Screenshot 제안**: Slack에서 배포 승인 요청 메시지가 온 화면 - 승인/거절 버튼이 보이는 상태 - ### 4. kustomize - 환경별 설정 분리 ``` @@ -180,7 +172,7 @@ base에 공통 리소스를 두고 overlay에서 환경별 차이만 덮어쓴 | **배포 트리거** | Image Updater (digest) | Image Updater (digest) | manifest 업데이트 + Slack 승인 | | **승인** | 없음 | 없음 | Slack 버튼 (n8n 경유) | | **ArgoCD sync** | 자동 | 자동 | 수동 (승인 후) | -| **롤백** | 재push | 재push | Git revert → 자동 sync | +| **롤백** | 재push | 재push | Git revert → 수동 sync | ## 삽질했던 것들 @@ -210,8 +202,6 @@ ApplicationSet에 서비스를 다시 추가하고 standalone Application을 삭 배포 직후에 ArgoCD가 자꾸 자동 롤백을 해서 뭔가 싶었는데, 원인은 readiness probe의 `initialDelaySeconds`가 너무 짧은 거였다. Spring Boot 앱이 완전히 뜨기 전에 헬스체크가 시작되니까 실패 → 롤백이 반복된 거다. probe 타이밍을 앱 기동 시간에 맞게 조정해서 해결했다. -> **Screenshot 제안**: ArgoCD에서 Degraded 상태로 표시된 Application 화면 (혹은 Pod의 Restart 횟수가 보이는 화면) - ## 성과 - **WAF 도입과 함께 보안 관련 CS 및 해킹 시도 0건** 달성 diff --git a/content/posts/ko/jdk-migration-strategy-11-to-25.md b/content/posts/ko/jdk-migration-strategy-11-to-25.md index c169e88..738fcd0 100644 --- a/content/posts/ko/jdk-migration-strategy-11-to-25.md +++ b/content/posts/ko/jdk-migration-strategy-11-to-25.md @@ -1,8 +1,8 @@ --- title: "JDK 마이그레이션 (1) - JDK 11에서 21로, Spring Boot 3.x 전환기" date: "2026-03-18" -updated: "2026-03-18" -description: "프로덕션 Spring Boot 서비스를 JDK 11 → 21로 업그레이드하며 겪은 javax→jakarta 전환, AWS SDK v3 마이그레이션, QueryDSL 호환성 해결 등 실전 경험을 정리합니다." +updated: "2026-10-13" +description: "프로덕션 Spring Boot 서비스를 JDK 11 → 21로 업그레이드하며 겪은 javax→jakarta 전환, AWS SDK for Java 1.x→2.x와 Spring Cloud AWS 3.x 마이그레이션, QueryDSL 호환성 해결 등 실전 경험을 정리합니다." tags: ["java", "jdk", "spring-boot", "migration", "devops"] series: "JDK Migration" seriesOrder: 1 @@ -23,7 +23,7 @@ draft: false **Spring Boot 2.7.x OSS 지원 종료**도 트리거였다. 보안 패치가 끊기면 프로덕션에서 운영할 명분이 없다. Spring Boot 3.x로 올리려면 JDK 17 이상이 필수이고 어차피 올릴 거면 LTS인 21까지 한 번에 가기로 했다. -**AWS SDK v1의 유지보수 모드 전환** 역시 무시할 수 없었다. SQS, S3, Lambda를 적극적으로 사용하는 서비스 특성상 새로운 AWS 기능이 SDK v1에 추가되지 않는 건 실질적인 제약이었다. 특히 SQS의 비동기 처리 개선이나 S3의 새로운 API를 활용하려면 SDK v3가 필요했다. +**AWS SDK for Java 1.x의 유지보수 모드 전환** 역시 무시할 수 없었다. SQS, S3, Lambda를 적극적으로 사용하는 서비스 특성상 새로운 AWS 기능이 SDK 1.x에 추가되지 않는 건 실질적인 제약이었다. AWS SDK for Java 2.x와 이를 사용하는 Spring Cloud AWS 3.x로 클라이언트와 통합 라이브러리를 함께 옮겼다. ### 변경 요약 @@ -33,7 +33,8 @@ draft: false | Spring Boot | 2.7.2 | 3.4.3 | | Gradle | 7.5 | 8.5 | | Java EE | javax.* | jakarta.* | -| AWS SDK | v1 (awspring 2.4) | v3 (awspring 3.3) | +| AWS SDK for Java | 1.x | 2.x | +| Spring Cloud AWS | 2.4.4 | 3.3.0 | | MySQL Connector | mysql:mysql-connector-java | com.mysql:mysql-connector-j | | QueryDSL | querydsl-jpa | querydsl-jpa:5.0.0:jakarta | @@ -102,7 +103,9 @@ annotationProcessor "jakarta.persistence:jakarta.persistence-api" 핵심은 `:jakarta` classifier다. 이것만 빠뜨려도 Q클래스 생성은 정상이지만 런타임에 javax 의존성을 찾다가 실패한다. 컴파일이 되니까 안심하고 배포했다가 프로덕션에서 터지는 최악의 시나리오가 가능하다. -## AWS SDK v1 → v3 (awspring cloud) +## AWS SDK for Java 1.x → 2.x, Spring Cloud AWS 3.x 전환 + +SDK와 Spring 통합 라이브러리의 버전을 구분해야 한다. AWS SDK for Java는 1.x에서 2.x로, Spring Cloud AWS는 2.4.4에서 3.3.0으로 올렸다. [Spring Cloud AWS 공식 호환성 표](https://awspring.io/what-is-spring-cloud-aws#compatibility-with-spring-project-versions)에서도 Spring Cloud AWS 3.3.x가 AWS SDK for Java 2.x를 사용한다고 명시한다. 가장 손이 많이 가는 변경이었다. 의존성, 설정 파일, 서비스 코드 세 곳 모두 변경이 필요하다. @@ -120,7 +123,7 @@ implementation "io.awspring.cloud:spring-cloud-starter-aws-messaging" implementation "io.awspring.cloud:spring-cloud-starter-aws" implementation "com.amazonaws:aws-java-sdk-lambda" -// after (AWS SDK v3 기반) +// after (AWS SDK for Java 2.x 기반의 Spring Cloud AWS 3.x) implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.3.0") implementation 'io.awspring.cloud:spring-cloud-aws-starter:3.3.0' implementation 'io.awspring.cloud:spring-cloud-aws-sqs:3.3.0' @@ -128,6 +131,8 @@ implementation 'io.awspring.cloud:spring-cloud-aws-s3:3.3.0' implementation 'software.amazon.awssdk:lambda:2.30.27' ``` +[AWS 공식 마이그레이션 가이드](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/migration-whats-different.html)에서 설명하는 패키지 변경도 같은 구분이다. SDK 1.x의 `com.amazonaws`가 SDK 2.x의 `software.amazon.awssdk`로 바뀌었다. 위 코드의 `3.3.0`은 Spring Cloud AWS 버전이고, Lambda 클라이언트의 `2.30.27`은 AWS SDK for Java 버전이다. + ### 설정 파일 구조 변경 프로퍼티 네임스페이스가 `cloud.aws.*`에서 `spring.cloud.aws.*`로 이동한다. @@ -159,20 +164,22 @@ spring: ### 서비스 코드 마이그레이션 -AWS SDK v3는 비동기 클라이언트가 기본이다. 클라이언트 생성 방식과 API 호출 패턴이 근본적으로 달라진다. +AWS SDK for Java 2.x는 동기 `S3Client`와 비동기 `S3AsyncClient`를 모두 제공한다. 모든 호출이 기본적으로 비동기가 되는 것은 아니다. [AWS S3 클라이언트 안내](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/examples-s3.html)를 기준으로 호출부에 맞는 클라이언트를 선택해야 한다. 아래는 기존 `AmazonS3` 업로드를 `S3AsyncClient`로 옮기는 예다. ```java // S3 - before (sync, blocking) AmazonS3 s3Client; s3Client.putObject(bucket, key, file); -// S3 - after (async, non-blocking) +// S3 - after (AWS SDK for Java 2.x, async) S3AsyncClient s3Client; -s3Client.putObject(PutObjectRequest.builder() - .bucket(bucket).key(key).build(), +CompletableFuture upload = s3Client.putObject( + PutObjectRequest.builder().bucket(bucket).key(key).build(), AsyncRequestBody.fromFile(file)); ``` +비동기 업로드는 `CompletableFuture`를 반환하므로 호출부에서 완료와 실패를 함께 처리해야 한다. 클라이언트 생성, 리전, 인증 정보도 API 호출 변경과 별도로 확인한다. + ```java // SQS Listener // before @@ -206,9 +213,9 @@ Spring Boot 3.x에서 `WebSecurityConfigurerAdapter`가 완전히 제거되었 가장 찾기 어려웠던 버그다. Q클래스 생성도 정상, 컴파일도 정상인데 런타임에 `ClassNotFoundException`이 터진다. `:jakarta`를 빠뜨리면 QueryDSL이 내부적으로 `javax.persistence`를 참조하기 때문이다. 컴파일 타임에 잡히지 않으니 CI에서도 통과하고 로컬 테스트에서도 통과하고 실제 기동할 때 터진다. -### AWS SDK v3 비동기 전환 +### AWS SDK for Java 2.x 비동기 클라이언트 전환 -SDK v1의 동기 API에 익숙해져 있으면 v3의 `CompletableFuture` 기반 비동기 패턴이 낯설 수 있다. 기존 동기 코드를 그대로 `.join()`으로 감싸는 것도 방법이지만 장기적으로는 비동기 패턴을 도입하는 것이 좋다. +동기 호출이 필요한 경로에는 `S3Client`를 쓸 수 있고, `S3AsyncClient`를 쓰는 경로는 반환된 future의 완료와 실패를 처리해야 한다. `.join()`은 호출 스레드를 기다리게 하므로, 비동기 클라이언트로 바꾸고 바로 `.join()`한다고 호출부까지 논블로킹이 되는 것은 아니다. ### 219개 파일을 한 번에 바꾼 것 @@ -216,7 +223,7 @@ SDK v1의 동기 API에 익숙해져 있으면 v3의 `CompletableFuture` 기반 ## 마무리 -JDK 11 → 21은 단순한 버전업이 아니라 **Java 생태계 전체의 패러다임 전환**이었다. javax → jakarta, AWS SDK v1 → v3, Spring Security 설정 방식 변경까지 동시에 일어나기 때문에, 실질적으로는 프로젝트를 새로 세팅하는 것에 가깝다. +JDK 11 → 21은 단순한 버전업이 아니라 **Java 생태계 전체의 패러다임 전환**이었다. javax → jakarta, AWS SDK for Java 1.x → 2.x, Spring Cloud AWS 2.x → 3.x, Spring Security 설정 방식 변경까지 동시에 일어나기 때문에, 실질적으로는 프로젝트를 새로 세팅하는 것에 가깝다. 219개 파일을 한 번에 바꿨지만 프로덕션에 무사히 올라갔을 때의 안도감은 아직도 생생하다. diff --git a/content/posts/ko/jdk-migration-strategy-21-to-25.md b/content/posts/ko/jdk-migration-strategy-21-to-25.md index 66d6d70..3d1f159 100644 --- a/content/posts/ko/jdk-migration-strategy-21-to-25.md +++ b/content/posts/ko/jdk-migration-strategy-21-to-25.md @@ -1,7 +1,7 @@ --- title: "JDK 마이그레이션 (2) - JDK 21에서 25로, Virtual Threads와 모던 Java" date: "2026-03-18" -updated: "2026-03-18" +updated: "2026-10-13" description: "JDK 21 → 25 업그레이드 실전기. Virtual Threads 활성화, pinning 이슈 해소, pattern matching switch 적용, Gradle Kotlin DSL 전환까지." tags: ["java", "jdk", "spring-boot", "virtual-threads", "devops"] series: "JDK Migration" diff --git a/content/posts/ko/jwt-hs256-to-rs256-jwks-kms.md b/content/posts/ko/jwt-hs256-to-rs256-jwks-kms.md index 76e5cc4..43cbab1 100644 --- a/content/posts/ko/jwt-hs256-to-rs256-jwks-kms.md +++ b/content/posts/ko/jwt-hs256-to-rs256-jwks-kms.md @@ -1,7 +1,7 @@ --- title: "JWT 서명 전환 - HS256에서 RS256/JWKS/KMS로" date: "2026-04-24" -updated: "2026-04-24" +updated: "2026-10-13" description: "대칭키 JWT 서명을 RS256/JWKS/KMS 구조로 바꾸며 서명 책임, kid, key rotation, legacy 토큰 처리를 어떻게 나눴는지 정리합니다." tags: ["authentication", "jwt", "jwks", "kms", "java", "spring", "security"] draft: false diff --git a/content/posts/ko/lgtm-stack-observability.md b/content/posts/ko/lgtm-stack-observability.md index 38a018c..4ccc7ba 100644 --- a/content/posts/ko/lgtm-stack-observability.md +++ b/content/posts/ko/lgtm-stack-observability.md @@ -1,7 +1,7 @@ --- title: "Datadog 걷어내고 LGTM 스택 구축하기 - OTel Sidecar + Grafana/Loki/Tempo/Mimir" date: "2026-03-17" -updated: "2026-03-17" +updated: "2026-10-13" description: "Datadog 비용 압박으로 오픈소스 LGTM 스택(Grafana, Loki, Tempo, Mimir)으로 전환한 경험을 공유합니다. OTel Collector 사이드카 설계부터 노이즈 필터링, MDC 기반 요청 추적, IRSA 기반 S3 저장소 구성까지 실전 구축기입니다." tags: ["grafana", "loki", "tempo", "mimir", "opentelemetry", "kubernetes", "observability", "devops"] series: "서비스 인프라 현대화" diff --git a/content/posts/ko/lgtm-stack-setup-guide.md b/content/posts/ko/lgtm-stack-setup-guide.md index 8dd7f23..a0d20b3 100644 --- a/content/posts/ko/lgtm-stack-setup-guide.md +++ b/content/posts/ko/lgtm-stack-setup-guide.md @@ -1,7 +1,7 @@ --- title: "EKS에 LGTM 스택 구축하기 - Loki, Grafana, Tempo, Mimir 실전 셋업 가이드" date: "2026-03-17" -updated: "2026-03-17" +updated: "2026-10-13" description: "EKS 클러스터에 Grafana LGTM 스택을 Helm으로 배포하고 OTel Collector 사이드카로 Spring Boot 앱의 로그·트레이스·메트릭을 수집하는 전체 과정을 다룹니다." tags: ["grafana", "loki", "tempo", "mimir", "opentelemetry", "kubernetes", "helm", "observability"] draft: false diff --git a/content/posts/ko/multi-channel-notification-server.md b/content/posts/ko/multi-channel-notification-server.md index 2977cb7..cfc1980 100644 --- a/content/posts/ko/multi-channel-notification-server.md +++ b/content/posts/ko/multi-channel-notification-server.md @@ -1,7 +1,7 @@ --- title: "멀티채널 알림 서버 구축기 - php 레거시에서 Java/Spring으로" date: "2026-03-09" -updated: "2026-03-09" +updated: "2026-10-13" description: "PHP 파일에 흩어져 있던 알림 로직을 Spring Boot 기반 독립 알림 서버로 구축한 과정. 카카오 알림톡, SMS, 이메일, 푸시, Slack까지 6개 채널을 하나의 서버에서 처리하는 구조를 만들기까지." tags: ["java", "spring-boot", "notification", "sqs", "architecture"] draft: false diff --git a/content/posts/ko/odin-ax-transformation.md b/content/posts/ko/odin-ax-transformation.md index 7043e1f..1323d6c 100644 --- a/content/posts/ko/odin-ax-transformation.md +++ b/content/posts/ko/odin-ax-transformation.md @@ -1,7 +1,7 @@ --- title: "팀의 업무 병목을 줄이기 위한 AX 전환" date: "2026-09-22" -updated: "2026-09-22" +updated: "2026-10-13" description: "목적별 에이전트 실험을 거쳐, 한 세션에서 일하던 경험을 오딘과 Codex 하네스로 옮기기까지의 시행착오." tags: ["ai", "ai-agent", "codex", "slack", "ax", "engineering"] series: "AI 에이전트와 일하는 시스템 만들기" diff --git a/content/posts/ko/payment-system-redesign-sync-api.md b/content/posts/ko/payment-system-redesign-sync-api.md index b755c12..a359cd2 100644 --- a/content/posts/ko/payment-system-redesign-sync-api.md +++ b/content/posts/ko/payment-system-redesign-sync-api.md @@ -1,7 +1,7 @@ --- title: "결제 시스템 재설계 (2) - SQS를 걷어내고 동기 API로" date: "2026-03-06" -updated: "2026-03-06" +updated: "2026-10-13" description: "이벤트 드리븐의 매력에 빠져 도입한 SQS였지만 결제 도메인에서는 오히려 복잡도만 높였다. SQS를 제거하고 동기 API + Portone 웹훅 기반으로 전환하면서 결제 프로세스를 안정화한 이야기." tags: ["java", "spring-boot", "payment", "portone", "refactoring"] series: "결제 시스템 재설계" diff --git a/content/posts/ko/spring-ai-cs-automation.md b/content/posts/ko/spring-ai-cs-automation.md index 27b1656..e39ba84 100644 --- a/content/posts/ko/spring-ai-cs-automation.md +++ b/content/posts/ko/spring-ai-cs-automation.md @@ -1,7 +1,7 @@ --- title: "Spring AI CS 상담 보조 시스템 운영기 - Function Calling에서 운영자 제안 워크플로까지" date: "2026-07-06" -updated: "2026-07-28" +updated: "2026-10-13" description: "Spring AI 기반 CS 답변 초안을 운영에 적용하며 Function Calling 중심 설계를 병렬 조회, QA, 오류 진단, 운영자 검수 워크플로로 바꾼 과정과 성과를 정리합니다." tags: ["spring-ai", "spring-boot", "ai", "llm", "cs"] draft: false diff --git a/content/posts/ko/spring-ai-guide-01-setup.md b/content/posts/ko/spring-ai-guide-01-setup.md index 4770679..34f3cc8 100644 --- a/content/posts/ko/spring-ai-guide-01-setup.md +++ b/content/posts/ko/spring-ai-guide-01-setup.md @@ -1,7 +1,7 @@ --- title: "Spring AI 적용 가이드 (1) - 프로젝트 설정부터 첫 번째 AI 호출까지" date: "2026-03-17" -updated: "2026-03-17" +updated: "2026-10-13" description: "Spring AI의 핵심 개념과 프로젝트 설정, ChatClient를 이용한 첫 번째 AI 호출까지를 정리합니다." tags: ["spring-ai", "spring-boot", "ai", "llm", "java"] series: "Spring AI 적용 가이드" diff --git a/content/posts/ko/spring-ai-guide-02-multi-provider.md b/content/posts/ko/spring-ai-guide-02-multi-provider.md index 6b8c728..0350375 100644 --- a/content/posts/ko/spring-ai-guide-02-multi-provider.md +++ b/content/posts/ko/spring-ai-guide-02-multi-provider.md @@ -1,7 +1,7 @@ --- title: "Spring AI 적용 가이드 (2) - 멀티 프로바이더 전략" date: "2026-03-17" -updated: "2026-03-17" +updated: "2026-10-13" description: "OpenAI, AWS Bedrock, Google Gemini를 하나의 ChatClient 인터페이스로 추상화하고 런타임에 동적으로 전환하는 방법을 다룹니다." tags: ["spring-ai", "spring-boot", "ai", "openai", "bedrock", "gemini"] series: "Spring AI 적용 가이드" diff --git a/content/posts/ko/spring-ai-guide-03-prompt-structured-output.md b/content/posts/ko/spring-ai-guide-03-prompt-structured-output.md index 7d9bc07..8864725 100644 --- a/content/posts/ko/spring-ai-guide-03-prompt-structured-output.md +++ b/content/posts/ko/spring-ai-guide-03-prompt-structured-output.md @@ -1,7 +1,7 @@ --- title: "Spring AI 적용 가이드 (3) - 프롬프트 관리 & Structured Output" date: "2026-03-17" -updated: "2026-03-17" +updated: "2026-10-13" description: "Spring AI의 메시지 모델, 변수 템플릿링, DB 기반 프롬프트 관리, JSON Schema를 활용한 Structured Output을 다룹니다." tags: ["spring-ai", "spring-boot", "ai", "prompt-engineering", "structured-output"] series: "Spring AI 적용 가이드" diff --git a/content/posts/ko/spring-ai-pipeline-real-world.md b/content/posts/ko/spring-ai-pipeline-real-world.md index 0c74999..e7ad980 100644 --- a/content/posts/ko/spring-ai-pipeline-real-world.md +++ b/content/posts/ko/spring-ai-pipeline-real-world.md @@ -1,7 +1,7 @@ --- title: "Spring AI 실전 적용기 - 7단계 AI 진단 파이프라인 구축" date: "2026-02-28" -updated: "2026-02-28" +updated: "2026-10-13" description: "HTTP 직접 호출 + 동기 단일 프롬프트 방식을 Spring AI 기반 7단계 파이프라인으로 재설계한 실전 경험을 공유합니다." tags: ["spring-ai", "spring-boot", "ai", "llm"] draft: false diff --git a/content/posts/ko/spring-facade-pattern-layered-architecture.md b/content/posts/ko/spring-facade-pattern-layered-architecture.md index 4adba50..443be3b 100644 --- a/content/posts/ko/spring-facade-pattern-layered-architecture.md +++ b/content/posts/ko/spring-facade-pattern-layered-architecture.md @@ -1,7 +1,7 @@ --- title: "Spring MVC에 Facade 패턴 도입기 - @Gateway 어노테이션으로 레이어드 아키텍처 개선" date: "2026-03-10" -updated: "2026-03-10" +updated: "2026-10-13" description: "전통적인 Controller-Service-Repository 구조에 커스텀 @Gateway 어노테이션과 Facade 레이어를 도입하여 도메인별 단일 책임 원칙을 확보하고 팀 생산성을 높인 과정을 공유합니다." tags: ["java", "spring-boot", "architecture", "design-pattern", "facade"] draft: false diff --git a/content/posts/ko/sync-virtual-coroutine-benchmark.md b/content/posts/ko/sync-virtual-coroutine-benchmark.md index 5df6f18..2aae820 100644 --- a/content/posts/ko/sync-virtual-coroutine-benchmark.md +++ b/content/posts/ko/sync-virtual-coroutine-benchmark.md @@ -1,7 +1,7 @@ --- title: "Platform Thread vs Virtual Thread vs Coroutine - 10,000 태스크 벤치마크" date: "2026-03-24" -updated: "2026-03-24" +updated: "2026-10-13" description: "JDK 25 Platform Thread, Virtual Thread, Kotlin Coroutine의 I/O Bound, CPU Bound, High Concurrency 시나리오별 성능을 100회 반복 벤치마크로 비교한다." tags: ["java", "kotlin", "virtual-threads", "coroutine", "benchmark", "concurrency"] draft: false