DP 삼각형의 가장 왼쪽의 값 (DP[i][0])은 한 줄 위의 가장 왼쪽 값(DP[i - 1][0])에 현재 값(triangle[i][0])을 더한 값이 된다. DP[i][0] = DP[i-1][0] + triangle[i][0]이 됩니다.
삼각형의 가장 오른쪽의 값 (DP[i][i])은 한 줄 위의 가장 오른쪽 값(DP[i-1][i-1])에 현재 값(triangle[i][i])을 더한 값이 된다. DP[i][i] = DP[i-1][i-1] + triangle[i][i]이 됩니다.
삼각형의 1번과 2번을 제외한 나머지는 한줄 위의 오른쪽 값과 왼쪽 값 중 큰 값에 현재 값을 더한 값이 된다. DP[i][i] = Max(DP[i - 1][j - 1], DP[i - 1][j]) + triangle[i][j]가 됩니다.
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문제는 거의 동일해서 어렵지 않게 해결할 수 있었습니다. 알고리즘 고수가 되고 싶습니다.ㅠㅠ
위의 명령어를 실행하여 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를 나중에 사용할 수 있도록 환경 변수에 저장합니다.
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"의 값으로 변경합니다.
밑에 카나리아 릴리스 배포, 프로덕션에 배포는 생략하였습니다.
마지막 과정까지 완료하여서 쿠버네티스 구글 클라우드 과정을 수료하였습니다! 이 후에 쿠버네티스 구글 클라우드 과정의 후기와 느낀 점의 글을 작성하겠습니다!
괄호를 보고 stack을 이용해서 풀려고 했는데 괄호 문자를 입력으로 주지 않아 DFS의 성질을 이용해 풀었다. 올바른 괄호의 짝 중에, '('로 시작했으면 ')'로 끝나는 성질을 이용해 ')'의 개수가 '('보다 많으면 올바르지 않은 식으로 간주하고, 이 모든 경우의 수를 dfs로 찾았다.
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문제밖에 풀지 못하네요. 알고리즘 고수가 되는 그날까지 분발하겠습니다.
문제를 읽다 보면 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);
}
}
오늘의 회고
문제를 풀면 풀수록 부족하다는 생각이 드네요. 언제쯤 쉽게 문제를 해결할 수 있을까요...? 오늘은 맥북 충전 단자가 고장이나서 서비스 센터에 갔다 오느라 정신없었던 하루였습니다. 공부도 많이 못했습니다. 남은 시간이라도 열심히 공부하겠습니다.
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로 어떻게 컨트롤러에서 값을 꺼내는지 알아보겠습니다.
위처럼 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이라는 어노테이션을 사용한다는 점입니다.
@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 어노테이션을 사용하여 제거할 수 있습니다.
커맨드 객체의 역할
컨트롤러에서 View로 바인딩 : View 단에서 form:form 태그를 사용하는 경우
View에서 컨트롤러로 바인딩 : View 단에서 input type="text" 혹은 input type="hidden"으로 값을 컨트롤러로 전송하는 경우
컨트롤러에서 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 단에서도 꺼내 쓸 일이 있는 것들은 이런 식으로 처리해서 사용하기도 합니다.