728x90

step 2 - 4. 정수 삼각형


내가 떠올린 풀이 해설

  1. DP 삼각형의 가장 왼쪽의 값 (DP[i][0])은 한 줄 위의 가장 왼쪽 값(DP[i - 1][0])에 현재 값(triangle[i][0])을 더한 값이 된다.
    DP[i][0] = DP[i-1][0] + triangle[i][0]이 됩니다.
  2. 삼각형의 가장 오른쪽의 값 (DP[i][i])은 한 줄 위의 가장 오른쪽 값(DP[i-1][i-1])에 현재 값(triangle[i][i])을 더한 값이 된다.
    DP[i][i] = DP[i-1][i-1] + triangle[i][i]이 됩니다.
  3. 삼각형의 1번과 2번을 제외한 나머지는 한줄 위의 오른쪽 값과 왼쪽 값 중 큰 값에 현재 값을 더한 값이 된다.
    DP[i][i] = Max(DP[i - 1][j - 1], DP[i - 1][j]) + triangle[i][j]가 됩니다.
  4. DP 배열에서 max 값을 출력하기 위해 answer = Math.max(answer, dp[i][j])을 해준다.

정확한 풀이

public class step2_4 {

	public static void main(String[] args) {
		int[][] tri = {{7},
				{3, 8},
				{8, 1, 0},
				{2, 7, 4, 4},
				{4, 5, 2, 6, 5}};
		int [][]dp = new int[tri.length][tri[tri.length - 1].length];
		int answer = 0;
		int max = 0;
		dp[0][0] = tri[0][0];
		for(int i = 1; i < tri.length; i++) {
			dp[i][0] = dp[i - 1][0] + tri[i][0];
			dp[i][i] = dp[i - 1][i - 1] + tri[i][i];
			for(int j = 1; j <= i - 1; j++) {
				dp[i][j] = Math.max(dp[i - 1][j - 1], dp[i - 1][j]) + tri[i][j];
				answer = Math.max(answer, dp[i][j]);
			}
		}
		System.out.println(answer);
	}
}

오늘의 회고

오늘은 DP문제와 프로그래머스 위장 문제와 유사한 문제를 백준에서 찾아서 HashMap 문제를 풀었습니다. 점화식은 도출을 해냈지만 DP 배열을 만들 생각을 하지 못하고 triangle 배열에서 점화식을 처리해주려고 했는데 생각처럼 되지 않았습니다. HashMap문제는 거의 동일해서 어렵지 않게 해결할 수 있었습니다. 알고리즘 고수가 되고 싶습니다.ㅠㅠ

728x90
728x90

파일 관련 명령어

touch : 내용에 아무것도 없는 빈 파일을 생성

rm : 파일을 제거하는 명령어

  • 일반적으로 -rf 옵션과 같이 사용 -r은 디렉터리 삭제 -f는 강제 삭제

mv : 파일 이름 변경

  • mv [현재 파일 이름] [변경할 파일 이름]

mv : 파일 이동

  • mv [원본 경로] [이동할 경로]

cp : 파일 복사

  • cp[원본 이름] [복사할 이름]

cat, head, tail, more : 파일 내용 확인

cat : 파일의 내용을 전부 확인

  • -n : 파일 내용의 줄 을 같이 출력해준다.

head : 파일의 내용을 시작부터 몇 줄만 확인, 기본적으로 10줄, 옵션으로 보고 싶은 라인 수 지정

tail : 파일의 내용을 끝에서 몇 줄만 확인, 기본적으로 10줄, 옵션으로 보고 싶은 라인 수 지정

  • -f : 실시간으로 내용의 변경사항을 확인할 수 있다.

more : 파일의 내용을 화면에 맞춰서 확인, space는 한 페이지씩 확인, enter는 한 줄 더 확인

  • 보통 head 명령어를 안쓰고 cat /etc/passwd | head 이렇게 쓴다.

vi 편집기

command 모드

가장 기본이 되는 모드, 복사, 붙여 넣기, 다른 모드로 전환, 커서 이동, 삭제 등

(1) 커서 이동

  • 방향키
  • G : 가장 마지막 줄로 이동
  • gg : 첫번째 줄로 이동
  • [n]G : n번째 줄로 이동 ex) 100번째 줄 이동 100G
  • $ : 커서가 위치한 줄의 맨 끝으로 이동
  • 숫자 0 : 커서가 위치한 줄의 맨 앞으로 이동
  • w : 커서가 한 단어씩 오른쪽으로 이동
  • b : 커서가 한 단어씩 왼쪽으로 이동
  • h : 방향키 왼쪽
  • j : 방향키 아래
  • k : 방향키 위로
  • l : 방향키 오른쪽

(2) 삭제

  • x : 한 문자 삭제, 또는 Edit모드에서 백스페이스나 delete 키로 삭제도 가능
  • dd : 커서가 위치한 한 줄 삭제 ex) 5 dd 하면 5줄 한 번에 삭제
  • d[커서이동] : 커서가 이동한만큼 삭제

(3) 수정

  • r : 커서가 위치한 부분의 문자 하나를 입력하는 문자로 대체
  • u : Ctrl + z 처럼 작업 취소

(4) 복사 및 붙여넣기

  • yy : 커서가 위치한 한 줄을 복사
  • y [커서 이동] : 커서 이동하는 만큼 복사
  • p : 커서 밑이나 커서 다음에 붙여 넣기

Last Line 모드

파일을 저장하거나, 종료하거나 저장하지 않고 그냥 종료, 검색, 바꾸기 등

(1) 편집기 상태 변경

  • :set nu : 라인 넘버 표시
  • :set nonu : 라인 넘버 표시 안 함
  • :set ic : 검색할 때 대소문자 무시
  • :set noic : 검색할 때 대소문자 적용

(2) 검색 및 변환

  • /[내용] : 검색, n 다음 검색, N 이전 검색
  • :%s/[찾을내용]/[바꿀내용]/g

(3) 저장 및 종료

  • :w : 저장
  • :q : 종료
  • :wq : 저장 및 종료
  • :w! : 강제로 저장
  • :q! : 강제로 종료
  • :wq! : 저장 및 강제 종료
728x90

'개발 > Linux' 카테고리의 다른 글

프로세스, 아카이브(tar), 압축  (0) 2022.07.29
특수권한, 쉘 명령어  (0) 2022.07.28
링크, 권한 명령어  (0) 2022.07.27
grep, fgrep, find 명령어  (0) 2022.07.26
리눅스 기초, 디렉터리 관련 명령어  (0) 2022.07.21
728x90

개요

Kubernetes 엔진에서 지속적 전달 파이프라인을 설정하는 방법

  • Kubernetes Engine 클러스터에 Jenkins 애플리케이션 프로비저닝
  • Helm 패키지 관리자를 사용하여 Jenkins 애플리케이션 설정
  • Jenkins 애플리케이션의 기능 살펴보기
  • Jenkins 파이프라인 생성 및 실행

젠킨스란?

Jenkins는 빌드, 테스트 및 배포 파이프라인을 유연하게 오케스트레이션 할 수 있는 오픈 소스 자동화 서버입니다. Jenkins를 사용하면 개발자가 지속적 배포에서 발생할 수 있는 오버헤드 문제에 대해 걱정하지 않고 프로젝트를 빠르게 배포할 수 있습니다.

 

1. Jenkins 프로비저닝

gcloud container clusters create jenkins-cd \
--num-nodes 2 \
--machine-type n1-standard-2 \
--scopes "https://www.googleapis.com/auth/source.read_write,cloud-platform"

Kubernetes 클러스터 만들기위한 명령어입니다.

 

gcloud container clusters list

클러스터가 실행 중인지 확인하는 명령어입니다.

 

gcloud container clusters get-credentials jenkins-cd

클러스터에 대한 자격 증명을 가져옵니다.

 

kubectl cluster-info

Kubernetes Engine은 이러한 자격 증명을 사용하여 새로 프로비저닝 된 클러스터에 액세스 합니다. 위의 명령어를 실행하여 클러스터에 연결할 수 있는지 확인합니다.

 

2. Setup Helm

Helm을 사용하여 Charts 리포지토리에서 Jenkins를 설치합니다. Helm은 Kubernetes 애플리케이션을 쉽게 구성하고 배포할 수 있게 해주는 패키지 관리자입니다. Jenkins가 설치되면 CI/CD 파이프라인을 설정할 수 있습니다.

 

helm repo add jenkins https://charts.jenkins.io
helm repo update

Helm의 차트 저장소 추가하는 명령어입니다.

 

3. Jenkins 구성 및 설치

Jenkins 설치 시 values파일을 템플릿으로 사용하여 설정에 필요한 값을 제공할 수 있습니다.

사용자 지정 values파일을 사용하여 Kubernetes 클라우드를 자동으로 구성하고 다음 필수 플러그인을 추가합니다.

  • Kubernetes:1.29.4
  • Workflow-multibranch:latest
  • Git:4.7.1
  • Configuration-as-code:1.51
  • Google-oauth-plugin:latest
  • Google-source-plugin:latest
  • Google-storage-plugin:latest

이렇게 하면 Jenkins가 클러스터와 GCP 프로젝트에 연결할 수 있습니다.

 

gsutil cp gs://spls/gsp330/values.yaml jenkins/values.yaml
helm install cd jenkins/jenkins -f jenkins/values.yaml --wait

첫 번째 명령어는 values파일을 다운로드 하기 위한 명령어입니다.

두 번째 명령어는 Helm CLI를 사용하여 구성 설정으로 차트를 배포하는 명령어입니다.

 

kubectl get pods
kubectl create clusterrolebinding jenkins-deploy --clusterrole=cluster-admin --serviceaccount=default:cd-jenkins

클러스터에 배포할 수 있도록 Jenkins 서비스 계정을 구성합니다.

Jenkins 포드가 Running상태로 전환되고 컨테이너가 READY 상태인지 확인합니다.

export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/component=jenkins-master" -l "app.kubernetes.io/instance=cd" -o jsonpath="{.items[0].metadata.name}")
kubectl port-forward $POD_NAME 8080:8080 >> /dev/null &

위의 명령어를 실행하여 Cloud Shell에서 Jenkins UI로 포트 전달을 설정합니다.

 

kubectl get svc

Jenkins 서비스가 제대로 생성되었는지 확인합니다.

Jenkins 마스터가 요청할 때 빌더 노드가 필요에 따라 자동으로 시작되도록 Kubernetes 플러그인을 사용하고 있습니다. 작업이 완료되면 자동으로 종료되고 리소스가 클러스터 리소스 풀에 다시 추가됩니다.

이 서비스는 셀렉터와 일치하는 모든 포드에 대해 포트 8080 및 50000을 제공합니다. 이렇게 하면 Kubernetes 클러스터 내의 Jenkins 웹 UI 및 Builder/에이전트 등록 포트가 표시됩니다. 또한 jenkins-ui 서비스는 클러스터를 사용하여 노출됩니다.클러스터 외부에서 액세스할 수 없도록 IP를 지정합니다.
 

4. Jenkins에 연결

printf $(kubectl get secret cd-jenkins -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo

Jenkins 차트는 자동으로 관리자 암호를 생성합니다. 위의 명령어는 암호를 확인하기 위한 명령어입니다.

 

사용자 이름 admin과 자동 생성된 비밀번호로 로그인할 수 있습니다.

 

성공적으로 로그인이 된걸 확인할 수 있다.

 

6. 애플리케이션 배포

  • 프로덕션 : 사용자가 액세스하는 라이브 사이트입니다.
  • 카나리아 : 사용자 트래픽의 일정 비율만 수신하는 소규모 사이트입니다. 이 환경을 사용하여 모든 사용자에게 릴리스 되기 전에 라이브 트래픽으로 소프트웨어를 검증하십시오.

cd sample-app
kubectl create ns production
kubectl apply -f k8s/production -n production
kubectl apply -f k8s/canary -n production
kubectl apply -f k8s/services -n production

두 번째 명렁어로 배포를 논리적으로 격리하기 위해 Kubernetes 네임스페이스를 만듭니다.

kubectl apply 명령어를 사용하여 프로덕션 및 카나리아 배포와 서비스를 만듭니다.

 

기본적으로 프런트엔드의 복제본은 하나만 배포됩니다. 명령을 사용하여 kubectl scale항상 실행 중인 복제본이 4개 이상 있는지 확인합니다.

kubectl scale deployment gceme-frontend-production -n production --replicas 4
kubectl get pods -n production -l app=gceme -l role=frontend
kubectl get pods -n production -l app=gceme -l role=backend
kubectl get service gceme-frontend -n production

첫 번째 명령어를 실행하여 프로덕션 환경의 프런트엔드를 확장합니다.

이제 두 번째 명령어를 실행하여 프런트엔드용으로 5개의 포드, 프로덕션 트래픽용으로 4개, 카나리아 릴리스용으로 1개가 실행 중인지 확인합니다(카나리아 릴리스에 대한 변경 사항은 사용자 5명 중 1명(20%)에게만 영향을 미침).

세 번째 명령어를 실행하여 백엔드용 포드 2개(프로덕션용 1개, 카나리아용 1개)가 있는지 확인합니다.

네 번째 명령어는 프로덕션 서비스에 대한 외부 IP 검색하는 명령어입니다.

 

외부 IP 를 브라우저에 붙여 넣으면 카드에 표시된 정보 카드를 볼 수 있습니다.

 

export FRONTEND_SERVICE_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=production services gceme-frontend)
curl http://$FRONTEND_SERVICE_IP/version

첫 번째 명령어로 프런트엔드 서비스 로드 밸런서 IP를 나중에 사용할 수 있도록 환경 변수에 저장합니다.

브라우저에서 프런트엔드 외부 IP 주소를 열어 두 서비스가 모두 작동하는지 확인합니다.

curl 명령을 실행하여 서비스의 버전 출력을 확인합니다.

1.0.0 이 정상 출력되는 것을 확인하실 수 있습니다.

 

7. Jenkins 파이프라인 생성

git init
git config credential.helper gcloud.sh
git remote add origin https://source.developers.google.com/p/$DEVSHELL_PROJECT_ID/r/default
git config --global user.email "[EMAIL_ADDRESS]"
git config --global user.name "[USERNAME]"
git add .
git commit -m "Initial commit"
git push origin master

 

Multibranch Pipeline 옵션을 선택하고 확인을 클릭 합니다.

 

Branch Sources에서 소스 추가 를 클릭하고 git 을 선택합니다.

https://source.developers.google.com/p/[PROJECT_ID]/r/default

Project Repository에 위에 URL을 복사해 붙여 넣습니다. 

Credentials에서 이전 단계에서 서비스 계정을 추가할 때 생성한 자격 증명의 이름을 선택합니다.

 

Scan Multibranch Pipeline Triggers 섹션에서,  Periodically if not otherwise run을 선택하고 Interval 값을 1분으로 설정합니다. 이 단계를 완료하면 이라는 작업이 Branch indexing 실행됩니다.이 메타 작업은 저장소의 분기를 식별하고 기존 분기에서 변경 사항이 발생하지 않았는지 확인합니다. 왼쪽 상단의 sample-app을 클릭하면 master작업이 표시됩니다. Jenkins 파이프라인을 성공적으로 생성했습니다.

 

vi Jenkinsfile

REPLACE_WITH_YOUR_PROJECT_ID를 자기 자신의 PROJECT_ID값으로 바꾸고 저장을 해준다.

 

vi html.go

<div class="card blue">의 값을 <div class="card oragne"> 값으로 변경합니다.

 

vi main.go

const version string = "1.0.0"을 const version string = "2.0.0"의 값으로 변경합니다.

밑에 카나리아 릴리스 배포, 프로덕션에 배포는 생략하였습니다. 

 

마지막 과정까지 완료하여서 쿠버네티스 구글 클라우드 과정을 수료하였습니다! 이 후에 쿠버네티스 구글 클라우드 과정의 후기와 느낀 점의 글을 작성하겠습니다!

728x90
728x90

step 2 - 3. 올바른 괄호의 개수


내가 떠올린 풀이 해설

괄호를 보고 stack을 이용해서 풀려고 했는데 괄호 문자를 입력으로 주지 않아 DFS의 성질을 이용해 풀었다. 올바른 괄호의 짝 중에, '('로 시작했으면 ')'로 끝나는 성질을 이용해 ')'의 개수가 '('보다 많으면 올바르지 않은 식으로 간주하고, 이 모든 경우의 수를 dfs로 찾았다.

https://tosuccess.tistory.com/173

 

[프로그래머스 level_4] 올바른 괄호의 갯수 for JAVA

programmers.co.kr/learn/courses/30/lessons/12929 코딩테스트 연습 - 올바른 괄호의 갯수 올바른 괄호란 (())나 ()와 같이 올바르게 모두 닫힌 괄호를 의미합니다. )(나 ())() 와 같은 괄호는 올바르지 않은 괄..

tosuccess.tistory.com


정확한 풀이

import java.util.*;
public class step2_3 {
	static int answer;
	public static void main(String[] args) {
		int n = 2;
		answer = 0;
		DFS(0, 0, n);
		System.out.println(answer);
	}
	private static void DFS(int left, int right, int n) {
		if(left < right) {
			return;
		}
		if(left == n && right == n) {
			answer++;
			return;
		}
		if(left > n || right > n) {
			return;
		}
		DFS(left + 1, right, n);
		DFS(left, right + 1, n);
	}
}

오늘의 회고

오늘은 프로그래머스 3주 차 문제를 풀었습니다. DFS를 이용해서 푸는 문제였다. DFS 문제라고 떠올리기는 했지만 문제를 해결하는 방법의 아이디어가 떠오르지 않았다... 좀 더 넓은 방식으로 문제를 해결해야 되는데 어떻게 공부해야 될지 잘 모르겠다.ㅠㅠ

728x90
728x90

리눅스 기초

리눅스란?

  1. 컴퓨터 운영체제의 한 종류
  2. 윈도우와는 다르게 오픈 소스 운영체제
  3. 소스코드가 공개되어있기 때문에 다양한 리눅스 기반의 운영체제가 존재

 

특징

  1. 높은 이식성과 확장성(c언어 기반이기 때문)
  2. 안정성과 신뢰성
  3. 계층적 파일 시스템(최상의 디렉터리가 존재하고 모든 것들은 해당 디렉터리 하부에 존재)

 

프롬프트

  1. 컴퓨터가 입력을 기달리고 있음을 가리키기 위해 화면에 나타나는 표시
  2. 일반적으로 리눅스의 프롬프트는 현재 작업 디렉터리, 현재 로그인한 사용자 등에 대한 정보를 표시

 

$는 일반사용자

#은 관리자

 

man ls ls의 명령어에 대한 설명서

엔터 치면 한 줄씩, 스페이스 치면 한 페이지씩


디렉터리 관련 명령어

pwd : 현재 작업 디렉터리 확인
cd : 작업 디렉터리 변경
ls : 디렉터리 내용 확인
mkdir : 디렉터리 생성
rmdir : 디렉터리 제거
mv : 디렉터리 이름 변경
mv : 디렉터리 이동
cp : 디렉터리 복사

pwd : 현재 작업 디렉터리 확인

cd : 작업 디렉터리 변경

  • 절대 경로 : 최상의 디렉터리인 /부터 특정 파일 또는 디렉터리의 경로를 모두 입력
  • 상대 경로 : 현재 작업 디렉터리를 기준으로 특정 파일 또는 디렉터리의 경로를 입력 .은 현재 디렉터리를 의미하고, ..은 상위 디렉터리를 의미한다.

ls : 디렉터리 내용 확인

  • 일반적으로 ls는 -al 옵션과 같이 사용한다.
  • -a는 숨겨진 파일까지 모두 표시, -l 은 좀 더 자세한 결과를 출력한다.
  • -rwxr-xr-x : 파일에 대한 접근 권한
  • 1 : 하드 링크 수
  • root : 파일의 소유자 UID
  • root : 파일의 관리 그룹 GID
  • 4096 : 파일의 크기
  • .dockernev : 파일의 이름 파일 이름 앞에 . 이 붙어있으면 숨겨진 파일이다.

mkdir : 디렉터리 생성

  • -p 옵션을 사용하면 폴더 안에 폴더 안에 폴더 등 여러 폴더를 생성할 수 있다.

rmdir : 디렉터리 제거

  • rmdir은 폴더 안에 파일이 있으면 삭제가 안되고 비어 있어야 삭제가 된다.
  • 따라서 rm -r 옵션을 이용해서 파일을 삭제할 수 있다. rm은 rm -rf로 많이 사용

mv : 디렉터리 이름 변경

  • mv [현재 이름 디렉터리] [변경할 디렉터리 이름]

mv : 디렉터리 이동

  • mv [원본 경로][이동할 경로]

cp : 디렉터리 복사

  • 디렉터리를 통째로 복사할 때는 -r 옵션 사용
  • cp -r [원본 경로][이동할 경로]
728x90

'개발 > Linux' 카테고리의 다른 글

프로세스, 아카이브(tar), 압축  (0) 2022.07.29
특수권한, 쉘 명령어  (0) 2022.07.28
링크, 권한 명령어  (0) 2022.07.27
grep, fgrep, find 명령어  (0) 2022.07.26
리눅스 파일 관련 명령어, vi 명령어  (0) 2022.07.22
728x90

step 2 - 2. 게임 맵 최단거리


내가 떠올린 풀이 해설

DFS로 풀어도 답이 나오는 문제인데 저는 BFS로 풀었습니다. dx, dy로 상하좌우를 탐색할 배열을 만들어 준다. 또한 visit 배열을 만들어준다. visit배열은 BFS를 탐색하면서 거리가 1씩 늘어날수록 이동한 거리를 담아 줄 배열이다. 만약 answer가 0이면 -1, 아니면 answer를 리턴해준다. BFS 메서드에서는 BFS를 이용할 Queue를 생성하고 시작점인 0,0을 배열로 Queue에 add 해준다. Queue가 빌 때까지 반복한다. for문으로 상하좌우를 탐색하면서 만약 nx, ny가 0보다 같거나 크고, nx, ny가 map길이보다 작아야 하고, maps배열의 nx, ny위치가 1이고, visit배열의 nx, ny가 0이면 visit [nx][ny]에 visit [x][y] + 1 값을 넣어준다. 그리고 Queue에 nx, ny를 add 해준다.


정확한 풀이

import java.util.*;
public class step2_2 {
    static int[] dx = {1, 0, -1, 0};
    static int[] dy = {0, -1, 0, 1};
    static int answer;
    static int[][] visit;
	public static void main(String[] args) {
		int [][]maps = {{1, 0, 1, 1, 1},
				{1, 0, 1, 0, 1},
				{1, 0, 1, 1, 1},
				{1, 1, 1, 0, 1,},
				{0, 0, 0, 0, 1}};
		answer = 0;
		int [][] visit = new int[maps.length][maps[0].length];
		visit[0][0] = 1;
		BFS(maps, visit);  
		answer = visit[maps.length - 1][maps[0].length - 1];
		if(answer == 0) {
			System.out.println(-1);
		}
		System.out.println(answer);
	}
	private static void BFS(int[][] maps, int[][] visit) {
		Queue<int[]> que = new LinkedList<>();
		que.add(new int[] {0, 0});
		while(!que.isEmpty()) {
			int[] now = que.poll();
			int x = now[0];
			int y = now[1];
			for(int i = 0; i < 4; i++) {
				int nx = dx[i] + x;
				int ny = dy[i] + y;
				if(nx >= 0 && nx < maps.length && ny >= 0 && ny < maps[0].length && maps[nx][ny] == 1 && visit[nx][ny] == 0) {
					visit[nx][ny] = visit[x][y] + 1;
					que.add(new int[] {nx, ny});
				}
			}
		}
    }
}

오늘의 회고

요즘 계속 알고리즘 문제를 1문제밖에 풀지 못하네요. 알고리즘 고수가 되는 그날까지 분발하겠습니다. 

728x90
728x90

step 2 - 1. 위장


내가 떠올린 풀이 해설

문제를 읽다 보면 hashMap으로 풀어야 하는 단서가 되는 게 있다. 제한 사항에 같은 이름을 가진 의상은 존재하지 않습니다를 보고 떠올릴 수 있을 것 같다. 저는 hashMap의 key에 의상의 이름을 담고, value에 종류를 담아서 풀려고 했다. 하지만 key에 종류를 담고 value에 숫자를 담는다. 만약 담으려는 키가 존재하면 key의 value값을 리턴하고 만약 존재하지 않으면 default값을 반환하는 getOrDefault를 이용했다. 같은 종류의 의상은 1개밖에 입지 못하므로 경우의 수를 이용해서 풀었다. 


정확한 풀이

import java.util.*;

public class step2_1 {

	public static void main(String[] args) {
		String[][] clothes = {{"yellowhat", "headgear"}, 
								{"blue", "eyewear"}, 
								{"green_turban", "headgear"}};
		HashMap<String, Integer> map = new HashMap<>();
		int answer = 1;
		for(int i = 0; i < clothes.length; i++) {
			map.put(clothes[i][1], map.getOrDefault(clothes[i][1], 0) + 1);
        }
		Set<String> keySet = map.keySet();
		for(String key : keySet) {
			answer *= map.get(key) + 1;
		}
		System.out.println(answer - 1);
	}
}

오늘의 회고

문제를 풀면 풀수록 부족하다는 생각이 드네요. 언제쯤 쉽게 문제를 해결할 수 있을까요...? 오늘은 맥북 충전 단자가 고장이나서 서비스 센터에 갔다 오느라 정신없었던 하루였습니다. 공부도 많이 못했습니다. 남은 시간이라도 열심히 공부하겠습니다.

728x90
728x90

HttpServletRequest는 Http Servlet에 대한 요청 정보를 제공하도록 ServletRequest를 확장한 인터페이스이다.

HttpServletRequest는 서블릿 컨테이너가 생성하며 서블릿의 service() 메서드의 매개변수로 보냅니다.

서블릿의 생명주기(Life Cycle)

1. 서블릿 컨테이너가 서블릿 인스턴스의 init() 메서드를 호출하여 초기화한다.

  • 최초 요청시 한 번만 초기화되며 그 이후로는 이 과정을 생략한다.

2. 서블릿이 초기화된 다음부터 클라이언트가 요청을 처리할 수 있다. 각 요청은 별도의 스레드로 처리하고 이때 서블릿의 service() 메서드를 호출한다.

  • 이 안에서 HTTP 요청을 받고 클라이언트로 보낼 HTTP 응답을 만든다.
  • service()는 Http Method에 따라 doGet() 또는 doPost() 등으로 위임하여 처리한다.

3. 서블릿 컨테이너 판단에 따라 서블릿을 메모리에서 내려야 할 시점에 destroy() 를 호출한다.

public class HelloServlet extends HttpServlet {
  @Override
  public void init() throws ServletExcetion {
    System.out.println("init");
  }
  @Override
  public void doGet(HttpServletReqeust req, HttpServletResponse res) throws ServletExcetion {
    System.out.println("doGet");
  }
  @Override
  public void destory() {
    System.out.println("destroy");
  }
}

서블릿(servlet)은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱 합니다. 그리고 그 결과를 HttpServletRequest 객체에 담아서 제공합니다. 서블릿 덕분에 개발자들은 HTTP 스펙을 편리하게 사용할 수 있습니다. HttpServletRequest의 핵심 기능은 HttpServlet의 요청을 받아서 꺼내서 쓸 수 있다는 것입니다. 그럼 이제 HttpServletRequest로 어떻게 컨트롤러에서 값을 꺼내는지 알아보겠습니다.

<form id='actionForm' action="/board/user" method='post'>
  <input type="text" name="name" value="lusida" />
  <input type="text" name="age" value="26" />
</form>

위처럼 JSP에서 사람 정보를 입력하고 POST로 넘기면 formData 형식처럼 key와 value의 값으로 HttpServletRequest에 담아서 컨트롤러로 전달합니다.

  • content-type : application/x-www/form-urlencoded
  • 메시지 바디에 쿼리 파라미터 형식으로 전달 username=lusida&age=26

GET의 경우 URL 뒤에 /board/user?name=lusida&age=26 형식으로 전달되는데 물음표(?) 뒤의 문자열들을 쿼리스트링 또는 요청 파라미터라고 부릅니다. GET 방식의 경우에도 ?key=value&key=value 형식으로 HttpServletRequest에 담아서 컨트롤러로 전달합니다.

@PostMapping("/user")
public String user(HttpServletRequest request) {
  String name = request.getParameter("name"); // key 값을 이용해서 꺼내올 수 있다.
  String age = request.getParameter("age"); // key 값은 input 에서 설정한 name 값이다.
  
  // 만약에 JSP 에서 설정한 name="userName" 이라는 키값이 여러개인 경우에는
  // getParameterValues() 메서드를 이용하여 배열로 받아올 수 있다.
  String[] names = request.getParameterValues("name");
  
  return REDIRECT_LIST;
}

중요 : key 값은 input에서 설정한 name 값이다.

 

@RequestParam

HttpServletRequest과 동일하게 @RequestParam 은 1:1 방식입니다. 차이점은 HttpServletRequest 대신 @RequestParam이라는 어노테이션을 사용한다는 점입니다.

<form id='actionForm' action="/board/user" method='post'>
  <input type="text" name="name" value="lusida" />
  <input type="text" name="age" value="26" />
</form>
@PostMapping("/user")
public String user(@RequestParam String name, @RequestParam String age) {
  // @RequestParam 뒤에 붙는 매개변수 변수명은 JSP 에서 설정한 name 의 key 값과 동일해야 한다.
  return REDIRECT_LIST;
}

HttpServletRequest와 @RequestParam을 이용하여 받아오는 경우 요청 파라미터가 많아지면 많아질수록 컨트롤러 내부 코드나 매개변수가 증가하여 코드 가독성이 떨어지고, 작성되는 코드 양이 많아집니다. 이러한 문제를 해결하고자 나온 것이커맨드 객체(Command Object)입니다.

 

커맨드 객체

HttpServletRequest를 통해 들어온 요청 파라미터들을 setter 메서드를 이용하여 객체에 정의되어있는 속성에 바인딩이 되는 객체를 의미한다. 커맨드 객체는 보통 VO 나 DTO를 의미하며, HttpServletRequest로 받아오는 요청 파라미터의 key 값과 동일한 이름의 속성들과 setter 메서드를 가지고 있어야 합니다. 

 

어떻게 자동으로 바인딩을 시켜주냐 하면, 스프링이 내부적으로 HttpServletRequest와 커맨드 객체의 setter 메서드를 이용하여 알아서 바인딩시켜줍니다.

@Getter @Setter
public class User {
  private String name;
  private String age;
}
@PostMapping("/user")
public String user(User user, Model model) {
  String name = user.getName();
  String age = user.getAge();
  
  // user 파라미터를 model 에 담는다.
  model.addAttribute("user", user);
  return REDIRECT_LIST;
}

위에서 user 파라미터를 model에 담는 것을 볼 수 있습니다. 이 코드 또한 @ModelAttribute 어노테이션을 사용하여 제거할 수 있습니다.

 

커맨드 객체의 역할

  1. 컨트롤러에서 View로 바인딩 : View 단에서 form:form 태그를 사용하는 경우
  2. View에서 컨트롤러로 바인딩 : View 단에서 input type="text" 혹은 input type="hidden"으로 값을 컨트롤러로 전송하는 경우
  3. 컨트롤러에서 Mapper.xml로 바인딩 : Mapper.xml 에서 title = #{title}, contents = #{contents}처럼 사용하는 경우, 커맨드 객체를 통해 #{변수명}과 커맨드 객체의 필드명을 통해 바인딩해주는 경우

 

@ModelAttribute 

@ModelAttribute는 크게 메서드명 위에 사용되는 경우와 파라미터 옆에 사용되는 경우로 나뉩니다.

@ModelAttribute는 커맨드 객체와 같이 요청 파라미터들을 객체 프로퍼티에 바인딩시켜준다는 것입니다. 하지만 @ModelAttribute를 생략해도 커맨드 객체를 이용해서 바인딩이 되는데, @RequestParam 또한 생략해도 사실상 바인딩이 가능합니다. 그 이유는 스프링이 내부적으로 String이나 int 등은 @RequestParam으로 보고, 그 외의 복잡한 객체들은 @ModelAttribute 가 생략됐다고 간주하기 때문입니다. 하지만 그렇다고 무조건 생략하는 것은 위험합니다.

@PostMapping("/ins")
public String ins(@ModelAttribute User user, Model model) {
  String name = user.getUserName();
  String age = user.getAge();
  
  // user 객체를 모델에 담는 코드를 작성하지 않아도, 담겨져 있다.
  // 내부적으로 model.addAttribute("user", user); 로 담는다.
  // 만약 객체명과 변수명이 @ModelAttribute UserVo user 로 되어있는 경우에는 어떻게 담길까?
  // 클래스명을 기준으로 카멜케이스를 적용하여 model.addAttribute("userVo", user); 로 담는다.
  
  return REDIRECT_LIST;
}

@ModelAttribute의 역할 중 하나는 model 에 객체를 담아준다는 것입니다. 파라미터 객체 옆에 @ModelAttribute를 사용했을 때 얻는 또 다른 이점은 @ModelAttribute 가 붙은 파라미터를 처리할 때는 @RequestParam과 달리 검증(Validation) 작업을 내부적으로 진행합니다.

@RequestParam의 경우 스프링의 기본 타입 변환 기능을 이용해서 요청 파라미터 값을 메서드 파라미터 타입으로 변환하는데, 만약 숫자 타입의 파라미터라면 문자열 타입으로 들어온 요청 파라미터의 타입 변환을 시도하고 실패하면 Http 400 Bad Request 응답이 클라이언트로 가게 됩니다.

하지만 @ModelAttribute의 경우 내부적으로 검증(Validation) 작업을 진행하기 때문에 setter 메서드를 이용하여 값을 바인딩하려고 시도하다가 예외를 만나지만 작업이 중단되면서 Http 400 Bad Request 가 발생하지는 않습니다. 타입 변환에 실패해도 작업은 계속되며 BindingException 타입의 객체에 담겨서 컨트롤러로 전달됩니다. 보통 등록이나, 수정을 처리하는 핸들러 메서드의 경우 다양한 검증을 실시해야 하고, 사용자의 입력 값에 오류가 있을 때에는 이에 대한 처리를 컨트롤러에게 맡겨야 합니다.

따라서 @ModelAttribute를 통해서 폼의 정보를 전달받는 경우 Errors 객체나 BindingResult 객체를 @ModelAttribute 가 붙은 파라미터 바로 뒤에 선언해서 검증 처리를 실시합니다. Errors 나 BindingResult는 자신의 바로 앞에 있는 파라미터 검증에서 발생한 오류들만 전달해주기 때문에 @Valid 나 @Validated, @ModelAttribute 가 붙은 파라미터 바로 뒤에 선언되어야 합니다.

 

메서드 위에 @ModelAttribute 가 사용되는 경우

컨트롤러에서 메서드 위에 @ModelAttribute 가 사용되는 경우는, 해당 컨트롤러 내의 어떠한 핸들러 메서드들보다 먼저 동작하게 됩니다.

/**
 * @ModelAttribute 메서드가 먼저 동작하기 때문에,
 * 다른 핸들러 메서드에서 model 에 담겨져있는 user 키값을 이용하여 user 객체를 꺼내서 쓸 수 있다.
 */
@ModelAttribute("user")
public String initUser() {
  // 내부적으로 model.addAttribute("user", userService.findUser(FIRST_USER_SEQ)); 형태로 담는다.
  return userService.findUser(FIRST_USER_SEQ); 
}

따라서 여러 핸들러 메서드에서 공통으로 쓰이며, View 단에서도 꺼내 쓸 일이 있는 것들은 이런 식으로 처리해서 사용하기도 합니다.

 

참고 블로그

https://medium.com/webeveloper/modelattribute-%EC%99%80-%EC%BB%A4%EB%A7%A8%EB%93%9C-%EA%B0%9D%EC%B2%B4-command-object-42c14f268324

 

@ModelAttribute 와 커맨드 객체(Command Object)

@ModelAttribute 와 커맨드 객체(Command Object), @RequestParam 과 HttpServletRequest

medium.com

 

728x90

+ Recent posts