ConfigMap & Secret

일반적으로 배포 환경은 1~N개의 환경을 가진다. 개인이 운영하는 서비스에서도 최소한 개발 환경과 운영 환경으로 나눌 필요가 있고 회사에서 담담하는 서비스도 파트에서만 개발계와 검증계 2 가지 환경으로 운영하며 협력하는 파트와 개발계 / 검증계 / 유지보수계 / 운영계 총 4개의 환경으로 운영한다. 각 환경마다 환경 변수나 설정 값들이 다 다를 것인데 이때 쿠버네티스에서 환경 별로 값들을 다르게 할 수 있는 용도로 ConfigMap과 Secret을 제공한다.

1. ConfigMap

ConfigMap은 Key-Value 쌍의 형태로 데이터를 저장하는 데 사용하는 쿠버네티스 오브젝트이다. 보통 컨피그맵을 생성하고 파드에 해당 컨피그맵을 주입하는 방식으로 파드에서 컨피그맵을 사용한다. 파다의 환경변수에 컨피그맵의 값을 바인딩하거나 파드 볼륨을 생성해서 볼륨 마운팅을 통해 파일 형태로 컨피그맵을 사용할 수 있다.

2. Secret

컨피그맵은 평문을 사용하기 때문에 암호, 토큰, 키 등 중요한 정보를 직접 적는 것은 바람직하지 않다. 컨피그맵과 유사하지만 특히 기밀 데이터를 보관하기 위해서 사용된다. 인코딩은 base64를 이용해서 하게 되는데 base64는 누구나 디코딩 할 수 있기 때문에 완벽하게 안전다고 할 수는 없다. 하지만 평문보다는 안전한 것은 맞으며 추가적인 플러그인을 통해서 암호화를 하는 것이 좋다. 기본 데이터 저장소 etcd에 암호화되지 않은 상태로 저장되기 때문에 etcd에 접근을 제한하는 것 역시 중요하다. 특히, 외부에서는 접속하는 것을 제한하는 것 좋다.

ConfigMap & Secret 사용하는 3가지 방법

1. Literal

ConfigMap을 yml파일에 정의하거나 커맨드라인 입력을 통해 생성하는 방법이다.

kubectl create configmap hello-cm --from-literal=language=java

apiVersion: v1
kind: ConfigMap
metadata:
  name: literal-cm
data:
  today: monday
  month: october

2. File

각각의 환경 변수들을 공유할 수 있지만 파일 형태로 만들어서 Pod에 공유하는 방법도 있다.
pod1이 참조하고 있는 file-c.txt의 내용이 바뀌어도 Pod의 환경변수는 변하지 않는다. Pod가 재성성 되어야만 바뀐 ConfigMap이 적용된다.

kubectl create configmap cm-file --from-file=./properties/profile.properties

key는 파일명이며 value는 파일 내용이다.

apiVersion: v1
kind: Pod
metadata:
  name: pod1
spec:
  containers:
  - name: container
    image: example/test
    env:
    - name: config-env
      valueFrom:
        configMapKeyRef:
          name: file-cm
          key: config.txt

3. VolumeMount

Pod생성 시 mount경로를 정의하고, 해당 경로 안에 ConfigMap파일을 연결시켜 주는 방식이다. File을 ConfigMap으로 설정하는 것과 마찬가지로 ConfigMap을 정의해둔다. 생성하고자 하는 Pod의 volumes에 volume의 이름과 사용하고자 하는 configMap의 이름을 적어준다. containers 부분에 마운트할 볼륨과 마운트 경로를 적어주면 된다. 위의 File을 ConfigMap으로 설정하는 것과 달리 파일을 내용이 변하게 되면 Pod의 재생성 없이도 변경된 데이터를 즉시 반영할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: pod2
spec:
  containers:
  - name: container
    image: example/test
    volumeMounts:
    - name: volume1
      mountPath: /mount
  volumes:
  - name: volume1
    configMap:
      name: file-cm

쿠버네티스 기본 오브젝트 -1

Pod

파드는 쿠버네티스에서 생성하고 관리할 수 있는 배포 가능한 가장 작은 컴퓨터 단위이다. 공유 네임스페이스와 공유 파일시스템 볼륨이 있는 컨테이너들의 집합과 비슷하며 일반적으로 파드는 직접 생성하지는 않으며, 대신 워크로드 리소스를 사용하여 생성한다. 파드 안에는 독립적인 서비스를 구동할 수 있는데 서비스 간의 포트가 중복될 수는 없다. 또한, 파드가 생성될 때는 IP는 자동 할당되며 재생성시 변경된다.

1. Container

실행하는 각 컨테이너는 반복 가능하다. 의존성이 포함된 표준화는 어디에서 실행하던지 동일한 동작을 얻는다는 것을 의미한다.

컨테이너는 기본 호스트 인프라에서 애플리케이션을 분리한다. 따라서 다양한 클라우드 또는 OS 환경에서 보다 쉽게 배포할 수 있다.

쿠버네티스 클러스터에 있는 개별 node는 해당 노드에 할당된 파드를 구성하는 컨테이너들을 실행한다. 파드 내부에 컨테이너들은 같은 노드에서 실행될 수 있도록 같은 곳에 위치하고 함께 스케줄된다.

2. Label

모든 객체에 라벨을 연결할 수 있고 라벨 별로 구분 짓기 위함이다.

예를 들어, 이렇게 6개의 Pod가 있을 경우 3개의 Label은 Dev, 다른 3개의 Label은 production으로 환경을 나눠서 운영할 수 있다.

3. Node Schedule

쿠버네티스에서 스케줄링은 Kubelet이 파드를 실행할 수 있도록 파드가 노드에 적합하지 확인하는 것을 말한다. 스케줄링 결정을 위해 고려해야 할 요소에는 개별 및 집단 리소스 요구사항, 하드웨어 / 소프트웨어 / 정책 제한조건, 어피니티 및 안티-어피니티 명세, 데이터 지역성(data locality), 워크로드 간 간섭 등이 포함된다.

Service

실행중인 애플리케이션을 네트워크 서비스로 노출하는 추상화 방법이며 쿠버네티스를 사용하면 익숙하지 않은 서비스 디스커버리 메커니즘을 사용하기 위해 애플리케이션을 수정할 필요가 없다. 쿠버네티스는 파드에게 고유한 IP 주소와 파드 집합에 대한 단일 DNS 명을 부여하고, 그것들 간에 로드-밸런스를 수행할 수 있다.

1. ClusterIP

서비스를 클러스터-내부 IP에 노출시킨다. 이 값을 선택하면 클러스터 내에서만 서비스에 도달할 수 있다. 이것은 서비스의 type을 명시적으로 지정하지 않았을 때의 기본값이다.

2. NodePort

고정 포트 (NodePort)로 각 노드의 IP에 서비스를 노출시킨다. 노드 포트를 사용할 수 있도록 하기 위해, 쿠버네티스는 type: ClusterIP인 서비스를 요청했을 때와 마찬가지로 클러스터 IP 주소를 구성한다.

3. Load Balancer

클라우드 공급자의 로드 밸런서를 사용하여 서비스를 외부에 노출시킨다. 서비스를 외부에서 접근 가능하게 하기 위해서 Load Balancer 방식을 사용해야한다.

Volume

쿠버네티스는 다양한 유형의 볼륨을 지원한다. 파드는 여러 볼륨 유형을 동시에 사용할 수 있다. 임시 볼륨 유형은 파드의 수명을 갖지만, 퍼시스턴트 볼륨은 파드의 수명을 넘어 존재한다. 파드가 더 이상 존재하지 않으면, 쿠버네티스는 임시(ephemeral) 볼륨을 삭제하지만, 퍼시스턴트(persistent) 볼륨은 삭제하지 않는다. 볼륨의 종류와 상관없이, 파드 내의 컨테이너가 재시작되어도 데이터는 보존된다.

1. emptyDir

emptyDir 볼륨은 파드가 노드에 할당될 때 처음 생성되며, 해당 노드에서 파드가 실행되는 동안에만 존재한다. 이름에서 알 수 있듯이 emptyDir 볼륨은 처음에는 비어있다. 파드 내 모든 컨테이너는 emptyDir 볼륨에서 동일한 파일을 읽고 쓸 수 있지만, 해당 볼륨은 각각의 컨테이너에서 동일하거나 다른 경로에 마운트될 수 있다. 어떤 이유로든 노드에서 파드가 제거되면 emptyDir 의 데이터가 영구적으로 삭제된다.

emptyDir 의 일부 용도는 다음과 같다.

  • 디스크 기반의 병합 종류와 같은 스크레치 공간
  • 충돌로부터 복구하기위해 긴 계산을 검사점으로 지정
  • 웹 서버 컨테이너가 데이터를 처리하는 동안 컨텐츠 매니저 컨테이너가 가져오는 파일을 보관

2. hostPath

hostPath 볼륨은 호스트 노드의 파일시스템에 있는 파일이나 디렉터리를 파드에 마운트 한다. 이것은 대부분의 파드들이 필요한 것은 아니지만, 일부 애플리케이션에 강력한 탈출구를 제공한다.

예를 들어, hostPath 의 일부 용도는 다음과 같다.

  • 도커 내부에 접근할 필요가 있는 실행중인 컨테이너. /var/lib/docker 를 hostPath 로 이용함
  • 컨테이너에서 cAdvisor의 실행. /sys 를 hostPath 로 이용함
  • 파드는 주어진 hostPath 를 파드가 실행되기 이전에 있어야 하거나, 생성해야 하는지 그리고 존재해야 하는 대상을 지정할 수 있도록 허용함

경고:

HostPath 볼륨에는 많은 보안 위험이 있으며, 가능하면 HostPath를 사용하지 않는 것이 좋다. HostPath 볼륨을 사용해야 하는 경우, 필요한 파일 또는 디렉터리로만 범위를 지정하고 ReadOnly로 마운트해야 한다.

3. PVC / PV

persistentVolumeClaim 볼륨은 퍼시스턴트볼륨을 파드에 마운트하는데 사용한다. 퍼시스턴트볼륨클레임은 사용자가 특정 클라우드 환경의 세부 내용을 몰라도 내구성이있는 스토리지 (GCE 퍼시스턴트디스크 또는 iSCSI 볼륨와 같은)를 “클레임” 할 수 있는 방법이다.

퍼시스턴트볼륨 (PV)은 관리자가 프로비저닝하거나 스토리지 클래스를 사용하여 동적으로 프로비저닝한 클러스터의 스토리지이다. 노드가 클러스터 리소스인 것처럼 PV는 클러스터 리소스이다. PV는 Volumes와 같은 볼륨 플러그인이지만, PV를 사용하는 개별 파드와는 별개의 라이프사이클을 가진다. 이 API 오브젝트는 NFS, iSCSI 또는 클라우드 공급자별 스토리지 시스템 등 스토리지 구현에 대한 세부 정보를 담아낸다.

퍼시스턴트볼륨클레임 (PVC)은 사용자의 스토리지에 대한 요청이다. 파드와 비슷하다. 파드는 노드 리소스를 사용하고 PVC는 PV 리소스를 사용한다. 파드는 특정 수준의 리소스(CPU 및 메모리)를 요청할 수 있다.

참고 자료

https://kubernetes.io/ko/

Vagrant를 통해 쿠버네티스(K8S) 구축하기

쿠버네티스 사용하기 위한 몇 가지 방법

1. Virtual Machine에 Master node와 Worker node를 직접 구축
2. AWS, Azure, GCP 등 쿠버네티스 서비스를 사용
3. 웹 형태로 제공되는 쿠버네티스 서비스 이용

두 번째와 세 번째 방법이 편하지만 일정 금액을 지불해야 한다는 단점이 존재합니다. 또한, 쿠버네티스 환경을 잘 모르기 때문에 쿠버네티스 환경을 직접 구축해보면서 어떻게 구성되어 있는지 알아볼 겸 직접 구축하기로 했습니다.

VM에 직접 OS를 설치하고 쿠버네티스 클러스터를 구축하는 것은 많은 시간이 소요가 됩니다. 또한, 설치 순서가 틀린 경우 다시 구축하는 것이 빠를 정도로 순서가 복잡합니다. 그래서 설치 방법을 템플릿화하는 것이 좋다고 생각 되었고 vagrant를 사용해서 쿠버네티스 클러스터를 구축한다면 시간 단축도 되고 일관성 가지면서 클러스터를 구축할 수 있다는 사실을 알았습니다. vagrant는 IaC(Infrastructure as Code)의 줄인 말이고 코드를 사용해서 VM에 OS를 설치하고 서버를 배포할 수 있도록 자동화하는 도구입니다.

Vagrant를 사용해 쿠버네티스 클러스터 구축

1. VM Provider 설치 및 Vagrant 설치

VM Provider로 VirtualBox를 사용하기로 했고 버전은 7.0.14입니다
VirtualBox 다운 링크: https://www.virtualbox.org/wiki/Downloads

Vagrant는 Window 환경으로 다운을 받았고 버전은 2.4.1입니다
Vagrant 다운 링크: https://developer.hashicorp.com/vagrant/install

2. Vagrant Script 작성 및 실행

먼저, 생성할 VM 스펙을 정의합니다. 대표적으로 OS, network, provider (hyper-v, virtual box, vmware), resources(cpu, memory)가 있습니다. master node는 2CPU, 4GB MEM, worker node는 2CPU, 2GB MEM로 자원을 할당할 예정입니다. 저의 PC 사양은 CPU는 11th Gen Intel(R) Core(TM) i7-1165G7, Memory는 16GB입니다. 그 후 설치 스크립트를 작성합니다. 저는 master와 worker 의 공통으로 설치되는 부분을 먼저 정의하고 이후 master node에 필요한 스크립트만 따로 정의했습니다. VM에는 ssh로 접속할 예정이라서 gui 옵션은 껐습니다.

아래는 실제로 사용한 스크립트입니다.

$pre_install = <<-SCRIPT
  echo ">>>> pre-install <<<<<<"
  echo 'root:password1234' | sudo chpasswd
  sudo echo "192.168.1.100 master" >> /etc/hosts
  sudo echo "192.168.1.101 worker1" >> /etc/hosts
  sudo echo "192.168.1.102 worker2" >> /etc/hosts
  sudo sysctl net.ipv4.ip_forward=1
  sudo modprobe br_netfilter
  sudo swapoff -a
  
  echo ">>>> Install Containerd <<<<<<"
  sudo apt-get update
  sudo apt-get install ca-certificates curl gnupg -y
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
  sudo chmod a+r /etc/apt/keyrings/docker.gpg
  sudo echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
    $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
    sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  sudo apt-get update
  sudo apt-get install containerd.io
  sudo echo "" > /etc/containerd/config.toml
  sudo systemctl restart containerd

  echo ">>>> Install K8s Component <<<<<<"
  sudo apt-get install -y apt-transport-https
  sudo curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-archive-keyring.gpg
  echo "deb [signed-by=/etc/apt/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
  sudo apt-get update
  sudo apt-get install -y kubelet kubeadm kubectl
  sudo apt-mark hold kubelet kubeadm kubectl
SCRIPT

$mater_config = <<-SCRIPT
  echo ">>>> Master Node Config <<<<<<"
  sudo ssh-keyscan worker1 >> ~/.ssh/known_hosts
  sudo ssh-keyscan worker2 >> ~/.ssh/known_hosts
  sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address 192.168.1.100 2>&1 | tee /root/kubeadm_init_output.txt
  sudo mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config
  export KUBECONFIG=/etc/kubernetes/admin.conf
SCRIPT


Vagrant.configure("2") do |config|
  # Master 노드 설정
  config.vm.define "master" do |master|
    master.vm.box = "bento/ubuntu-22.04"
    master.vm.network "private_network", ip: "192.168.1.100"
    master.vm.provider "virtualbox" do |v|
      v.name = "Master"
      v.gui = false
      v.memory = 4096
      v.cpus = 2
    end
    master.vm.hostname = "master"
    master.vm.provision "shell", inline: $pre_install
    master.vm.provision "shell", inline: $mater_config
  end

  # Worker 노드 설정
  (1..2).each do |i|
    config.vm.define "worker#{i}" do |worker|
      worker.vm.box = "bento/ubuntu-22.04"
      worker.vm.network "private_network", ip: "192.168.1.11#{i}"
      worker.vm.provider "vmware_desktop" do |v|
        v.name = "Worker#{i}"
        v.gui = false
        v.memory = 2048
        v.cpus = 2
      end
      worker.vm.hostname = "worker#{i}"
      worker.vm.provision "shell", inline: $pre_install
    end
  end
end

3. 쿠버네티스 클러스터 구축 마무리

한 대의 master node와 두 대의 worker node가 설치되었다면 worker node를 master node에 join 시켜야 합니다.
master node에서 sudo 권한으로 tail -n 2 kubeadm_init_output.txt 명령을 실행하면 master node에 join할 수 있는 hash 값이 나오게 되는데 해당 값을 그대로 worker node에 적용하면 됩니다.
master node에 join

적용 후 쿠버네티스 노드들을 조회해보면 master node에서 worker node의 상태를 확인할 수 있습니다. 현재는 네트워크 연결이 되어 있지 않기 때문에 NotReady 상태입니다. master node에서 calico를 적용하게 되면 노드 간의 네트워크가 연결됩니다.

마지막으로, master node에서 kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml 명령어를 실행하게 되면 쿠버네티스 클러스가 구축됩니다.


4. 쿠버네티스를 구축할 때 몇 가지 주의할 점

첫번째로 VM을 생성할 때는 최소 사용이 2 CPU, 2 memory를 가져야 한다는 것입니다.
두번째로 swap off를 해줘야 하는데 쿠버네티스는 자신의 자원을 최대한 활용하는 것이 목적이기 때문에 swap에 대한 지원이 따로 없기 때문입니다. 충분한 자원이 없어서 swap를 사용하게 된다면 master 노드에서 아래와 같은 에러를 만날 수 있습니다.

swapoff를 안 했을 경우 오류

충분한 자원을 가지고 쿠버네티스 클러스터를 구축한다면 네트워크를 연결을 제외하면 NotReady인 상태로 클러스터가 구축이 됩니다. 이후 클러스터 간의 네트워크 연결을 하게 된다면 Ready 상태로 Node간의 통신을 할 수 있는 클러스터 환경이 만들어지게 됩니다.

전반적인 쿠버네티스 클러스터 구성하는 방법에 대해서 알아봤으며 다음장부터는 쿠버네티스의 세부적인 내용에 대해서 다룰 예정입니다.

참고 자료

https://alive-wong.tistory.com/67

워드프레스 Google SEO 적용한 글 작성 및 구글 서치 콘솔 등록하기

Google SEO(Search Engine Optimization)는 포스트가 Google 검색 결과에서 상위에 노출되도록 최적화하는 작업을 의미합니다. Google SEO가 잘 적용된 포스트는 높은 확률로 검색 결과 상위에 노출됩니다. 따라서 Google SEO 점수를 향상시키는 것은 매우 중요합니다. 또한, 워드프레스로 사이트를 운영하기 전에 구글 검색에서 노출되기 위해 구글 서치 콘솔에 도메인을 등록해야 합니다. 도메인을 등록하면 사용자들이 구글 검색을 통해 쉽게 사이트에 접근할 수 있습니다.

목차

1. 구글 서치 콘솔 등록 및 워드프레스 SEO 플러그인 설치
2. Google SEO 최적화

구글 서치 콘솔 등록 및 워드프레스 SEO 플러그인 설치

1-1. 구글 검색 콘솔에 접속하기

https://search.google.com/search-console/about?hl=ko

 

구글 서치 콘솔, Google Search Console

해당 페이지에 접속 후 시작하기 클릭!

1-2. 속성 추가 및 사이트 URL  주소 등록

속성 추가

속성 추가 클릭!

URL 등록

URL 접두어 선택 후 사이트 URL 주소를 입력하고 계속 버튼 클릭

1-3. 소유권 확인 절차

소유권 확인

HTML 태그 선택 및 메타 태그 복사

1-4. 헤더 코드 삽입 플러그인 설치 및 코드 삽입

insert headers and footers

Insert Headers and Footers 플러그인 설치

Header 적용

복사한 메타 태그를 복사 붙여넣기 후 SAVE 버튼 클릭!

소유권 확인 완료

구글 검색 콘솔 소유권으로 돌아와 확인 버튼 클릭 및 소유권 확인

1-5. 워드프레스 SEO 플러그인 설치

요스트 SEO

Yoast SEO 설치

1-6. 색인 검색

URL 검사

URL 검사

postX 사이트맵

PostX Plugin 사용으로 인해 페이지 색인 자동 생성

 

Google SEO 최적화

구글 SEO 적용 전

Google SEO 최적화 전

  1. 이미지에 대한 대체 텍스트 및 메타 정보가 없음.
  2. 외부 링크 및 내부 링크 부재
  3. SEO title 길이
  4. KeyPhrase 길이
  5. Meta description 길이
  6. 전체 Text 길이

구글 SEO 적용 후

Google SEO 최적화 후

 

참고 링크
yoast seo 설명

 

 

워드프레스 익숙해지기

인용 예시 입니다.

Name

Position

Description for this block. Use this space for describing your block. Any text will do. Description for this block. You can use this space for describing your block.

안녕하세요 텍스트 공간입니다.

안녕하세요 텍스트 공간입니다.

안녕하세요 헤드라인 테스트 입니다.

컨테이너를 사용하지 않고 이렇게 할 수 있습니다

public void doSomething() {
    log.info("do!!");
}

Filter & Interceptor

java

Filter

Filter란?

Filter는 클라이언트의 요청이 서블릿에 도달하기 전(pre-processing)과 응답이 클라이언트에게 돌아가기 전(post-processing)에 작업을 수행합니다.

  • 사용자 인증, 요청 정보 로깅, 데이터 암/복호화, 헤더 검사(XSS 방어) 등과 같은 전처리 또는 후처리 작업에 사용됩니다.
  • Filter는 서블릿 컨테이너(Tomcat 등)에 의해 관리됩니다. 이는 스프링의 빈(Bean)과는 다른 관리 영역이라는 의미입니다. 따라서 Filter에서 스프링의 의존성 주입(Dependency Injection)과 같은 특정 스프링 기능을 직접 사용하기 어렵습니다. 하지만 스프링에서는 Filter를 빈으로 등록하여 스프링의 일부 기능을 활용할 수 있습니다.

Body 수정에 대한 주의:

  • Request body를 수정할 때에는 주의가 필요합니다. 그러나 Request body의 데이터는 InputStream에 있기 때문에, 한 번 읽고 나면 다시 읽을 수 없습니다. 이로 인해 실제로는 Request body를 수정하기 어려울 수 있습니다. 이 문제를 해결하기 위해서는 ServletRequestWrapperHttpServletRequestWrapper를 사용하여 요청을 래핑하고, 래핑된 객체에서 새로운 InputStream을 제공하는 방식을 사용해야 합니다.

예외 처리:

  • Filter에서의 예외 처리는 직접 구현해야 합니다. 서블릿 컨테이너는 일반적으로 Filter에서 발생한 예외를 적절히 처리하지 않으므로, 예외 처리는 개발자에게 달려 있습니다.

대표적인 사용 예시

  • 로그인 인증 필터
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Value("${jwt.secret}")
    private String jwtSecret; // application.properties 또는 application.yml에서 JWT 시크릿 키 설정

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // HTTP 헤더에서 토큰을 가져오기
        String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) {
            // "Bearer " 이후의 토큰 부분만 추출
            String token = header.substring(7);

            // 토큰을 검증하고 클레임(사용자 정보)을 추출
            Claims claims = Jwts.parser()
                    .setSigningKey(jwtSecret)
                    .parseClaimsJws(token)
                    .getBody();

            // 클레임에서 사용자명을 추출
            String username = claims.getSubject();

            if (username != null) {
                // 추출한 사용자명을 기반으로 인증 객체 생성
                Authentication auth = new UsernamePasswordAuthenticationToken(username, null, null);
                // SecurityContextHolder에 인증 객체 저장
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }

        // 다음 필터로 전달
        filterChain.doFilter(request, response);
    }
}
  • 인코딩 필터
public class EncodingFilter implements Filter {

    private String encoding;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 필터 초기화 시 호출되는 메소드
        encoding = filterConfig.getInitParameter("encoding");
        if (encoding == null) {
            encoding = "UTF-8"; // 기본 인코딩은 UTF-8로 설정
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 필터가 실제로 수행하는 로직
        request.setCharacterEncoding(encoding);
        response.setCharacterEncoding(encoding);
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        // 필터 종료 시 호출되는 메소드
    }
}

Interceptor

Interceptor란?

  • 인터셉터는 Dispatcher Servlet이 컨트롤러에 매핑되기 전, 후에 요청과 응답을 가로채서 전처리 및 후처리 작업을 수행하는 Spring Framework의 기능입니다.
  • 공통 작업 수행, 권한 확인, 로깅 등의 비즈니스 로직을 처리하여 컨트롤러에 도달하기 전에 필요한 작업을 수행합니다.

관리 영역 및 장점:

  • 인터셉터는 Spring Container의 관리를 받아 스프링의 모든 bean 객체에 접근 가능하며, Spring 기능을 사용할 수 있다는 것을 의미합니다.
  • 스프링 컨테이너의 관리를 받아서 예외 처리에 관련된 다양한 기능들을 활용할 수 있습니다.

대표적인 사용 예시

  • 로그인 인터럽트
public class AuthenticationInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 사용자의 로그인 상태를 체크하는 로직
        boolean userLoggedIn = checkUserLoggedIn(request);

        if (userLoggedIn) {
            return true; // 계속 진행
        } else {
            // 로그인이 필요한 페이지에 접근하려고 할 때 로그인 페이지로 리다이렉트
            response.sendRedirect("/login");
            return false; // 요청 중단
        }
    }

    private boolean checkUserLoggedIn(HttpServletRequest request) {
        // 실제로는 세션 또는 인증 토큰을 확인하여 사용자의 로그인 상태를 체크하는 로직
        // 여기에서는 간단히 true를 반환하도록 가정
        return true;
    }
}
  • 로깅 인터럽트
public class LoggingInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 컨트롤러 메소드 호출 전에 실행되는 로직
        // 요청 시작 시간 로깅
        log.info("Request started at: " + System.currentTimeMillis());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 컨트롤러 메소드 호출 후에 실행되는 로직
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 뷰 렌더링까지 완료된 후에 실행되는 로직
        // 요청 종료 시간 로깅
        log.info("Request completed at: " + System.currentTimeMillis());
    }
}

2023년 회고

글또

(1) 낯선 환경에 적응하기

‘적응’, 2023년 인생을 가장 대표하는 한 단어이다. 1월 초 신규 부서에 와서 클라우드 사용만 해봤던 내가 클라우드를 제공하는 입장이 되었다. 클라우드 인프라에 대한 지식 습득, 새로운 사람들과의 만남, 회사라는 곳에 적응하는 등 많은 것들이 낯설고 새로웠다. 또한, 회사의 보안 규정이 너무 심해서 방화벽 신청, 재택을 위한 VDI 접속 후 Local pc에 접속 해야하는 등 네트워크, 보안 관련해서도 낯선 부분이 많았다. 이전 회사에서 다짐 하길 새로운 회사에 가게 되면 누구보다 빠르게 도메인 파악을 하자 라는 것이었다. 이런 생각을 하게 된 이유는 이전 회사에서 약 1년 동안 도메인 파악을 다 하지 못했기 때문이다. 도메인 파악을 못한 페인 포인트를 생각해보면 패션 유통이라는 생소한 도메인과 DB 중심의 서비스로 인해 디버깅이 힘들었기 때문이다. 다행히 이번 회사는 어플리케이션 중심의 서비스였고 도메인 리더 분께서 충분히 도메인을 설명을 해주셨기 때문에 기본 지식에 대한 뼈대를 잘 세울 수 있어서 도메인 이해가 상대적으로 잘 되었던 것 같다. (그래도 6개월이라는 시간이 걸렸다…) 이번 년도 내에 도메인에 대한 깊은 이해를 끝마치고 내년부터는 개발 지식 관련된 내용을 학습하는 것이 목표였으나 인프라 도메인은 깊게 들어가면 갈 수록 알아야 할 부분이 너무 많았다. 특히, 네트워크 가상화 부분이 무궁무진했다. 내년에는 가상화라는 큰 주제와 물리 장비라는 두 가지 도메인에 대한 학습을 해야겠다고 다짐을 해본다.

(2) 새로운 기술 적용하기

도메인 뿐만 아니라 새로운 IT 프레임워크와 아키텍처를 배운 시기이기도 하다. 4월부터 10월까지 Webflux, Event Driven Development, Hexagonal Archtecture에 대한 개념 학습 및 신규 프로젝트를 진행했다. Webflux란 spring mvc를 활용해 동기 방식으로 개발하는 하는 것이 아닌 node.js 처럼 모든 것이 비동기 방식으로 개발할 수 있게 하는 프레임 워크이다. 하나의 톰캣 서버는 기본 200개의 세션을 제공하는데 만약 동시에 1000개의 요청이 발생하면 모두 처리할 수 없다. 이때 webflux를 사용한다면 요청이 비동기로 처리 되기 때문에 보다 효율적으로 동시 요청을 처리할 수 있다. 하지만 webflux를 사용하면 java를 사용해서 개발하던 방식과 완전히 다른 개발을 하게 된다. 기본적으로 Mono, Flux를 통한 Wrapper를 통해서 데이터를 주고 받고 요청부터 응답까지 block을 할 수 없기 때문에 요청 순서가 정해져 있는 것은 webflux로 처리하기 어렵기 때문이다. (callback 구조를 사용해서 순서 보장을 해줘야 하기 때문에 코드량이 상당히 많아진다.) 그렇기 때문에 개발 생산성과 성능 개선을 잘 따져서 신중히 프레임 워크를 정해야한다.

Event Driven Development에서 가장 인상 깊었던 점은 이벤트에 대한 처리 책임은 publish가 아닌 subscribe에 있다는 점이다. publish는 이벤트를 방행하면 그 역할은 끝난 것이다. 발행된 이벤트를 처리하는 것은 subscribe이다. 이 말은 publish와 subscribe간의 의존성이 없다는 것이다. 예를 들어. 1개의 publish가 있고 3개의 subscribe가 있을 때 3개의 subscribe는 이벤트를 구독하지만 messageKey 등을 이용해서 자신이 처리할 이벤트가 아니라면 관여하지 않는 것이다. 즉, EDD의 장점은 서비스간의 결합이 느슨하다는 점이다. 이벤트를 발행하는 서비스는 이벤트 발행을 하면 책임을 다한 것이며 이벤트를 구독하는 서비스는 자신이 처리해야할 이벤트가 오면 처리만 하면 되는 것이다. 이렇게 서비스 간의 분산 처리를 고려할 때 EDD를 사용할 수 있다. 반대로 하나의 서비스에서 내부 계층 간의 의존성을 줄이기 위한 방법은 Hexagonal Archtecture가 있다.

Hexagonal Archtecture는 port & Adapter archtecture인데 간단하게 도메인을 가장 우선시하는 것이다. 일반적으로 Database 관점으로 설계를 하고 개발하는 경우가 많은데 이러면 domain entity가 DB에 종속적이게 된다. DB 변경이 발생하게 되면 관련된 어플리케이션 코드도 모두 수정하게 되어야한다는 단점이 있다. 또한, 새로운 개발자가 프로젝트에 투입 되었을 때 DB 중심으로 서비스를 파악해야 하기 때문에 서비스를 이해하는데 어려울 수 있다. 하지만 도메인 중심으로 서비스를 개발하면 도메인을 파악함으로써 서비스를 보다 쉽게 이해할 수 있다는 장점이 있다. 또한 Hexagonal archtecture를 사용하면 Adapter layer (controller, persistence, http 등) 외부와 통신하는 부분, Port Layer (인터페이스를 통한 도메인과 연결 시켜주는 부분)를 통해서 각 계층의 의존성을 확실하게 격리 시켜준다. 물론, 클래스, 인터페이스 관련된 코드 수가 2배 이상으로 많아 진다는 단점이 있다. 장기적으로 보면 유지 보수, 클린 코드 등 이점이 있지만 신속하게 개발 해야하는 프로젝트라면 생산성이 떨어진다는 단점이 존재한다. 약 6개월간 프로젝트를 하면서 배운 내용은 위와 같다.

(3) 개인적인 회고

한 가지 아쉬운 점은 좀 더 능동적인 자세가 있었다면 어땠을까 싶다… 효율적인 방법이 있으면 적극적으로 설득해서 도입을 한다던지 개발하면서 발생했던 이슈나 지식을 정리하면서 갔으면 조금 더 만족했을 프로젝트가 됐을 것 같다. 그래서 내년에는 개발을 하면서 얻은 지식을 틈틈이 정리 및 공유하면서 능동적인 자세를 가져야겠다고 다짐한다.

개인적으로 이번 년도에 부족했던 점은 개인 공부에 너무 소홀 했던 점이다. 클라우드에 대한 학습을 다짐했으나 막상 제대로 된 공부를 한 적도 없고 인프라 (switch, router, database, server 등) 같은 세부적인 학습도 하지 않았다. 뭔가…유독 이번 년도는 공부가 잘 되지 않았던 한 해이다. 집중도 잘 되지 않았고 그냥 놀고 싶었다……그래서 회사 업무에 집중만 하고 남는 시간은 그냥 즐겁게 보냈던 것 같다. 12월에 노트북을 켜고 무슨 공부를 할 까 생각을 해보는데 막상 무엇을 해야 할 지 감이 잡히질 않았다. 회사를 제외하고 어떤 개발자가 되어야 할지 궁극적으로 내가 추구하는 방향성은 무엇인지 정리가 필요하다고 생각한다. 단순히 업에 관련된 방향성이 아닌 인생의 방향성을 세워야 하는 시기가 온 것 같다. 방향성과 목표가 뚜렷하다면 무엇을 해야 할지 고민하는 것 보다 목표를 달성하기 위해서 시간 관리를 어떻게 해야 할지 생각하지 않을까??… 1월 초에 스페인 여행을 가서 개인적인 시간을 가지면서 생각을 해봐야겠다.

잘했던 점은 최선을 다해서 신규 프로젝트에 임했던 점이다. 신규 프로젝트를 진행하면서 나는 야생형 개발자라는 것을 다시 한번 깨달았는데 이론을 깊게 학습하고 개발하는 것이 아닌 개발을 하면서 경험을 통해서 개념을 체화 시키는 것이 나에게는 더 잘 맞는다는 것을 깨달았다. 때문에 IT 서적을 읽을 때도 빠르게 한번 훑어보거나 개발을 하면서 모르는 부분을 찾아보는 것이 나에게 더 잘 맞을 수도 있겠다라는 생각이 들었다. 개발적인 성장 뿐만 아니라 개인적으로 해보고 싶었던 것도 몇 가지 이루었다. 테니스 대회 나가보기, 수영 배우기, 나만의 블로그 사이트 가지기, 글또 참여하기!!!

글또를 시작하게 된 계기는 머리 속에 있는 지식에 대한 전달력을 높이고 싶기 때문이다. 생각을 하면서 글을 쓰다 보면 문장 구성, 단어 선택에 대한 생각이 많아지고 좋은 전달력을 가지게 된다고 확신을 한다. 이 가설이 틀렸을 수도 있지만 개발자에게 가설은 필수적인 존재니깐… 가설을 세우고 실행에 옮기고 보완하고를 반복하는 것이 좋은 개발자가 되는 것 뿐만 아니라 장기적인 인생에 있어서 많은 인사이트를 얻을 수 있지 않을까 싶다. 나는 다경험주의를 지향하니깐!

Junit5 로그 테스트하는 방법

junit5

소개

  • 테스트 코드를 작성할 때 로그가 정상적으로 출력 됐는지 검증하고자 할 때가 있다.
  • 테스트 코드에서 로그를 검증하고자 하는 2가지 방법에 대해서 알아보자.

첫번재 방법

Junit5 환경에서 OutputCaptureExtension.class 사용해서 콘솔에 출력되는 로그를 캡쳐 후 원하는 로그가 출력되었는지 확인하는 방법이다. 샘플 코드는 @Slf4j를 사용해서 로그를 출력한다.

Sample Service Code
아래 코드는 userId를 통해 UserInfo를 조회할 때 User가 없다면 예외 처리를 하는 로직이다. throw를 이용해 예외 처리를 넘기지 않고 단순히 log로만 예외 처리를 하는 방식이다.

@Slf4j
@Service
@RequiredArgsConstructor
public class userService {

                private final UserRepository userRepository;

		public UserInfo getUserInfo(Long userId) {
				try {
						UserInfo userInfo = userRepository.findUserInfo(userId);
						log.debug("success get userInfo")
						return userInfo;
				} catch(EntityNotFoundException e) {
						log.debug("failed get userInfo")
						log.warn("no entity By {}", userId);
						코드가 실패했을 때 로그가 정상적으로 찍혔는지 확인하는 방법이다.
			  }
		}
}

Sample Test Code

package com.test.junit;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;

@ExtendWith(OutputCaptureExtension.class)
public class userServiceTest {

    @Autowired
    UserService userService;

    @Test
    void given_userId_when_getUserInfo_then_generate_error_log   (CapturedOutput capturedOutput) {
	       
	        Long userId = 1L;
                userService.getUserInfo(userId);
                assertThat(capturedOutput.toString(), containsString("failed get userInfo"));
                assertThat(capturedOutput.toString(), containsString("no entity By 1"));
        
    }
}

결론

간단하게 로그에 대한 검증 테스트를 할 수 있다는 장점이 있고 application.yml 파일에서 로그 Level 설정을 통해서 원하는 Level의 로그만 따로 검증 테스트를 할 수 있다.


두번째 방법

LoggerFactory는 로그 메시지를 기록하는 Logger를 생성할 때 사용된다. Logger에 기록된 로그들을 Slf4j의 구현체인 ch.qos.logback를 통해서
실제로 출력된 로그들을 저장하고 검증할 수 있다.

Gradle Dependencies

dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    implementation 'ch.qos.logback:logback-classic:1.2.11' // Slf4j의 구현체
}

Sample Service Code

아래는 각각의 로그 level 별로 동일한 메시지를 출력하는 메소드이다.

package com.test.junit.log;


import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomLog {

    public void generateLogs(String msg) {
        log.trace(msg);
        log.debug(msg);
        log.info(msg);
        log.warn(msg);
        log.error(msg);
    }



}

Slf4j의 구현체인 logback를 통해서 실제로 출력된 로그들을 관리할 수 있는 클래스이다.
countEventsForLogger 메소드는 몇 개의 로그 출력 이벤트가 발생되었는지, search 메소드는 출력된 로그 msg와 level을 찾아서 List 형태로 반환해주며, contains는 단순히 msg와 level에 맞는 로그가 있는지 검증만 해준다.

package com.test.junit.log;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class MemoryAppender extends ListAppender<ILoggingEvent> {

    public void reset() {
        this.list.clear();
    }

    public boolean contains(String string, Level level) {
        return this.list.stream()
                .anyMatch(event -> event.toString().contains(string)
                        && event.getLevel().equals(level));
    }

    public int countEventsForLogger(String loggerName) {
        return (int) this.list.stream()
                .filter(event -> event.getLoggerName().contains(loggerName))
                .count();
    }

    public List<ILoggingEvent> search(String string) {
        return this.list.stream()
                .filter(event -> event.toString().contains(string))
                .collect(Collectors.toList());
    }

    public List<ILoggingEvent> search(String string, Level level) {
        return this.list.stream()
                .filter(event -> event.toString().contains(string)
                        && event.getLevel().equals(level))
                .collect(Collectors.toList());
    }

    public int getSize() {
        return this.list.size();
    }

    public List<ILoggingEvent> getLoggedEvents() {
        return Collections.unmodifiableList(this.list);
    }
}

Sample Test Code
LoggerFactory를 통해 원하는 클래스의 LogWatch를 시작하고 로그의 level 까지도 커스텀 할 수 있다는 장점이 있다.
테스트를 시작하기 전에 start() 메소드를 통해 LogWatch를 시작하고, 테스트가 끝나면 stop()으로 LogWatch를 종료한다.

package com.test.junit;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import com.test.junit.log.CustomLog;
import com.test.junit.log.MemoryAppender;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;

import static org.assertj.core.api.Assertions.assertThat;

public class CustomLogTest {

    MemoryAppender memoryAppender;

    @BeforeEach
    public void setup() {

        Logger logger = (Logger) LoggerFactory.getLogger(CustomLog.class);
        memoryAppender = new MemoryAppender();
        memoryAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory());
        logger.setLevel(Level.DEBUG);
        logger.addAppender(memoryAppender);
        memoryAppender.start();

    }

    @AfterEach
    public void teardown() {
        memoryAppender.stop();
    }

    @Test
    public void test() {

        LogTest001 logTest001 = new LogTest001();
        logTest001.generateLogs("MSG");
        assertThat(memoryAppender.countEventsForLogger(CustomLog.class.getName())).isEqualTo(4);
        assertThat(memoryAppender.search("MSG", Level.INFO).size()).isEqualTo(1);
        assertThat(memoryAppender.contains("MSG", Level.TRACE)).isFalse();
    }

}

결론

Application 코드에서 테스트할 클래스의 로그 출력이 정상적으로 되었는지 검증할 수 있고 application.yml 같은 설정 파일이 아닌
코드에서 watch할 Log의 Level을 설정할 수 있다.

느낀점

간편하게 로그 검증을 하기 위해서는 OutputCaptureExtension.class를 사용해서 검증하는 것이 좋고, 세세히 검증을 해야 할 때는 직접 @Slf4j의 구현체를 사용해 Custom한 LogWatch를 구현해서 사용하는게 좋아 보인다.