클래스로 구현된 스택과는 달리 자바에서 큐 메모리 구조는 별도의 인터페이스 형태로 제공됩니다.
큐를 상속받는 하위 인터페이스는 다음과 같습니다.
1) Deque<E>
2) BlockingDeque<E>
3) BlockingQueue<E>
4) TransferQueue<E>
큐 메모리 구조는 선형 메모리 공간에 데이터를 저장하면서 선입선출의 시맨틸을 따르는 자료구조입니다.
즉, 먼저 저장된 데이터가 먼저 인출됩니다.
Queue 메소드
Queue를 활용한 BFS 구현
import java.io.*;
import java.util.*;
/* 인접 리스트를 이용한 방향성 있는 그래프 클래스 */
class Graph {
private int V; // 노드의 개수
private LinkedList<Integer> adj[]; // 인접 리스트
/** 생성자 */
Graph(int v) {
V = v;
adj = new LinkedList[v];
for (int i=0; i<v; ++i) // 인접 리스트 초기화
adj[i] = new LinkedList();
}
/** 노드를 연결 v->w */
void addEdge(int v, int w) { adj[v].add(w); }
/** s를 시작 노드으로 한 BFS로 탐색하면서 탐색한 노드들을 출력 */
void BFS(int s) {
// 노드의 방문 여부 판단 (초깃값: false)
boolean visited[] = new boolean[V];
// BFS 구현을 위한 큐(Queue) 생성
LinkedList<Integer> queue = new LinkedList<Integer>();
// 현재 노드를 방문한 것으로 표시하고 큐에 삽입(enqueue)
visited[s] = true;
queue.add(s);
// 큐(Queue)가 빌 때까지 반복
while (queue.size() != 0) {
// 방문한 노드를 큐에서 추출(dequeue)하고 값을 출력
s = queue.poll();
System.out.print(s + " ");
// 방문한 노드와 인접한 모든 노드를 가져온다.
Iterator<Integer> i = adj[s].listIterator();
while (i.hasNext()) {
int n = i.next();
// 방문하지 않은 노드면 방문한 것으로 표시하고 큐에 삽입(enqueue)
if (!visited[n]) {
visited[n] = true;
queue.add(n);
}
}
}
}
}
- 목표
베스트 앨범에 들어갈 노래의 고유 번호를 순서대로 출력
- 노래를 수록하는 기준
1) 속한 노래가 많이 재생된 장르를 먼저 수록
2) 장르 내에서 많이 재생된 노래를 먼저 수록
3) 장르 내에서 재생 횟수가 같은 노래중에서 고유 번호가 낮은 노래를 먼저 수록
- 배열 설명
1) genres : 노래의 장르를 나타냄
2) plays : 노래별 재생횟수를 나타냄
- 해결과정
1) play 횟수를 중첩해서 더해준다.
2) 키 값만을 추출하여 리스트를 만들고, 리스트를 play횟수를 기준으로 정렬한다.
( 그 이유는 hashMap 은 순서가 없기 때문에 정렬할 수 없기 때문이다. )
3) key값을 정렬한 리스트에서 제일 많은 횟수를 재생한 장르부터 장르별 제일 많은 횟수가 플레이된 인덱스,
두번째로 많은 횟수가 플레이된 인덱스를 찾아 정답 배열에 순서대로 넣어준다.
4) 이때, 두번째로 많은 횟수가 플레이된 인덱스는 존재하지 않을 수 있기 때문에 이를 처리
5) 정답 리스트를 배열로 변환하여 반환
* 단, 장르별 두 곡까지 수록곡에 담을 수 있음
[ 소스코드 ]
import java.util.*;
public class BestElbum {
public static void main(String[] args) {
String[] genres = {"classic", "pop", "classic", "classic", "pop"};
int[] plays = {500, 600, 150, 800, 2500};
String resultArr = Arrays.toString(solution(genres, plays));
System.out.println(resultArr);
}
public static int[] solution(String[] genres, int[] plays) {
ArrayList<String> genre = new ArrayList<>();
Map<String, Integer> hashMap = new HashMap<>();
ArrayList<Integer> list = new ArrayList<>();
// 키 값이 이미 있으면 0을 반환하고 중첩되는 부분이 있으면 더해준다.
for (int i = 0; i < genres.length; i++) {
hashMap.put(genres[i], hashMap.getOrDefault(genres[i], 0) + plays[i]);
}
for (String key : hashMap.keySet()) {
genre.add(key);
}
Collections.sort(genre, (o1, o2) -> hashMap.get(o2) - hashMap.get(o1));
// key 값을 더해준 hashMap 에 대한 값을 내림차순으로 정렬한다.
for (int i = 0; i < genre.size(); i++) {
String g = genre.get(i); // 내림차순 정렬된 장르 리스트를 조회
int max = 0; // 장르의 음악에서 재생 횟수가 가장 큰 인덱스를 찾는다.
int firstIdx = -1;
for (int j = 0; j < genres.length; j++) { // 기존의 장르 배열만큼 반복
if(g.equals(genres[j]) && max < plays[j]){
// 장르 명이 동일하면 그 장르의 인덱스를 뽑아준다.
max = plays[j];
firstIdx = j;
}
}
max = 0;
int secondIdx = -1;
for (int j = 0; j < genres.length; j++) {
if (g.equals(genres[j]) && max < plays[j] && j != firstIdx) {
max = plays[j];
secondIdx = j;
}
}
list.add(firstIdx);
if (secondIdx >= 0) { // 장르에 대한 곡이 하나밖에 없는 경우는 -1로 남음
list.add(secondIdx);
}
}
int[] result = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
result[i] = list.get(i);
}
return result;
}
}
- 조건 1) 한 번에 한 개의 알파벳만 바꿀 수 있음 2) words 에 있는 단어로만 변환할 수 있음
- 알고리즘 dfs
- 해결 과정 1) 한 글자 빼고 나머지가 같은 단어를 words 에서 찾는다. 2) 찾은 단어를 visited = true 로 설정 3) cnt 를 증가시키며 dfs 함수를 재귀 호출 4) 모든 경우의 수를 보기 위해 check = false 로 재설정 5) begin 과 target이 같은 경우 cnt를 answer에 대입하고 종료
[ 소스코드 ]
public class wordChange {
static boolean[] check;
static int answer = 0;
public static void main(String[] args) {
String begin = "hit";
String target = "cog";
String[] words = {"hot", "dot", "dog", "lot", "log", "cog"};
answer = solution(begin, target, words);
System.out.println(answer);
}
public static int solution(String begin, String target, String[] words) {
check = new boolean[words.length];
dfs(begin, target, words, 0);
return answer;
}
public static void dfs(String begin, String target,String[] word, int cnt) {
if (begin.equals(target)) { // 타겟과 시작 단어가 같으면 바로 리턴
answer = cnt; // 두 글자 빼고 같은 경우 0 -> 1 -> 2로 증가함
return;
}
for (int i = 0; i < word.length; i++) {
int k = 0; // 같은 스펠링이 몇개인지 세기 위한 변수
if(check[i]){ // 한번 지나간 단어는 패스
continue;
}
for (int j = 0; j < begin.length(); j++) {
if(begin.charAt(j) == word[i].charAt(j)){
// begin 과 word 의 각 단어 내부를 확인
/*
ex. hit, dog, ["hot", "dot", "dog", "lot", "log", "cog"] 일 때,
begin : hit
word[0] : hot
k : 2
*/
k++;
}
}
/*
ex. k = 2 이므로,
check[0] = true;
dfs ( hot, cog, word, 1 );
check[0] = false;
*/
if (k == begin.length() - 1) { // 한글자 빼고 두 같은 경우만 보면 됨
check[i] = true; // 지나간 자리는 true
dfs(word[i], target, word, cnt + 1);
check[i] = false;
}
}
}
}
2) n만큼 for문을 돌리다가, check[i] 값이 false 인게 있으면 깊이 우선 탐색을 하는 dfs 메서드를 호출하고,
네트워크 연결 개수를 ++ 해준다.
* dfs 인자 : computer[][] , i , check[]
3) 전달받은 피라미터인 check[i] 값을 true로 바꿔준다.
4) computer[]의 길이만큼 반복문을 돈다.
5) 만약 아래 조건이 모두 만족하면, 재귀호출을 한다.
- 자기 자신이 아니다. ( i != j )
- check 배열 i 위치의 값이 false 이며, (check[i] == false )
- computer 배열의 값이 1인 것 ( computer[i][j] == 0 )
6) 2번으로 돌아간다.
7) answer을 리턴한다.
[ 작성 코드 ]
public class network {
/**
- 컴퓨터 개수 n
- 연결에 대한 정보가 담긴 2차원 배열 (computer)
- 출력 : 네트워크 개수
**/
public static void main(String[] args) {
int n = 3;
int[][] computer1 = {{1, 1, 0}, {1, 1, 0}, {0, 0, 1}};
int[][] computer2 = {{1, 1, 0}, {1, 1, 1}, {0, 1, 1}};
solution(n, computer1);
solution(n, computer2);
}
public static int solution(int n, int[][] computers) {
int answer = 0;
boolean[] check = new boolean[computers.length];
for (boolean i : check) {
i = false;
}
for (int i = 0; i < computers.length; i++) {
if (check[i] == false) {
dfs(computers, i, check);
answer++;
}
}
return answer;
}
public static boolean[] dfs(int[][] computers, int n, boolean[] check) {
// n : 방문 노드, computer : 네트워크 배열, check : 방문했는지 확인하는 배열
check[n] = true;
for (int i = 0; i < computers.length; i++) {
if(n != i && computers[n][i] == 1 && check[i] == false){
// n != i : 방문 할 노드의 인덱스와 방문한 노드가 같지 않음
// computer[n][i] : 네트워크 배열에서 이제 방문할 배열 원소가 1인 경우
// check[i] == false : 아직 방문 하지 않은 경우
check = dfs(computers, i, check);
}
}
return check;
}
}
일반적인 큐 구조를 가지면서, 데이터가 들어온 순서대로 나가는것이 아닌 우선순위를 먼저 결정하고
우선순위가 높은 데이터가 먼저 나가는 자료구조입니다.
우선순위 큐의 조건
필수적으로 Comparable Interface를 구현해야 한다.
- Comparable Interface를 구현하면 compareTo method 를 오버라이딩하게 되고,
객체에서 처리할 우선순위 조건을 리턴해주면 PriorityQueue가 알아서 우선순위 높은 객체를 추출하는 구조입니다.
Heap 을 이용해 구현하는 것이 일반적입니다.
- 데이터를 삽입할 때 우선순위를 기준으로 최대 힙 또는 최소 힙을 구성합니다.
데이터를 꺼낼 때 루트 노드를 얻어낸 뒤 루트 노드를 삭제할 때는 빈 루트 노드 위치에 맨 마지막 노드를 삽입 후 아래로 내려가면서
적절한 위치를 찾아 옮기는 방식으로 진행됩니다. ( * 최대힙과 최소힙 * )
우선순위 큐 특징
1. 높은 우선순위의 요소를 먼저 꺼내 처리하는 구조입니다.
2. 내부 요소는 힙으로 구성되어 이진트리 구조를 가집니다.
3. 우선순위를 중요시해야하는 상황에 주로 쓰입니다.
4. 시간복잡도는 O(NlogN) 입니다.
우선순위 큐 선언
Priority Queue 를 사용하면 java.util.PriorityQueue 를 import 합니다.
import java.util.PriorityQueue;
import java.util.Collections;
//낮은 숫자가 우선 순위인 int 형 우선순위 큐 선언
PriorityQueue<Integer> priorityQueueLowest = new PriorityQueue<>();
//높은 숫자가 우선 순위인 int 형 우선순위 큐 선언
PriorityQueue<Integer> priorityQueueHighest = new PriorityQueue<>(Collections.reverseOrder());
우선순위 큐 동작 방법
// add(value) 메서드의 경우 만약 삽입에 성공하면 true를 반환,
// 큐에 여유 공간이 없어 삽입에 실패하면 IllegalStateException을 발생
priorityQueueLowest.add(1);
priorityQueueLowest.add(10);
priorityQueueLowest.offer(100);
priorityQueueHighest.add(1);
priorityQueueHighest.add(10);
priorityQueueHighest.offer(100);
// 첫번째 값을 반환하고 제거 비어있다면 null
priorityQueueLowest.poll();
// 첫번째 값 제거 비어있다면 예외 발생
priorityQueueLowest.remove();
// 첫번째 값을 반환만 하고 제거 하지는 않는다.
// 큐가 비어있다면 null을 반환
priorityQueueLowest.peek();
// 첫번째 값을 반환만 하고 제거 하지는 않는다.
// 큐가 비어있다면 예외 발생
priorityQueueLowest.element();
// 초기화
priorityQueueLowest.clear();
- 출력: 꼭대기에서 바닥까지 이어지는 경로 중, 거쳐간 숫자의 합이 가장 큰 경우를 찾는다.
- 조건: 아래 칸으로 이동할 때에는 대각선 방향으로 한 칸 오른쪽 or 왼쪽으로만 이동 가능
- 해결 방법:
[ DFS or DP ] 이 중 DP를 활용해 중복해서 거쳐가는 곳의 값을 따로 저장해 사용합니다.
예를 들어서 설명해보면 위 그림에서
1) [ 8,1,0 ] 에서 1은 7+3 / 7+8 중 큰 값을 골라주면 됩니다.
2) [ 2,7,4,4 ] 에서
- 7은 (7+3+8) / (7+8+1) 중에 큰 값을 고르게 됩니다.
- 4는 (7+8+0) / (7+8+1) 중에 큰 값을 고르게 됩니다.
이렇게 계산은 맨 왼쪽값, 중간값, 맨 오른쪽 값을 나눠서 계산하게 됩니다.
그러면 저장될 배열인 dp[][] 에는 dp[i-1][j-1], dp[i-1][j] 중 큰 값 + 현재 선택된 값이 들어가게 됩니다.
- 점화식 : dp[i][j] = MAX(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
[ 작성 코드 ]
public static int solution(int[][] triangle) {
int answer = 0;
int[][] dp = new int[triangle.length][triangle.length];
dp[0][0] = triangle[0][0]; // 맨 위에 값을 넣어줌
for (int i = 1; i < triangle.length; i++) {
/* 맨 왼쪽 */
dp[i][0] = dp[i - 1][0] + triangle[i][0];
// 7+3, 7+8, 7+2 와 같이 맨 왼쪽 부분을 더하게 된다.
// dp[1][0] (10) = dp[0][0] (7) + triangle[1][0] (3) -- 1
// dp[2][0] (18) = dp[1][0] (10) + triangle[2][0] (8) -- 2
/* 중간값 */
for (int j = 1; j <= i; j++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1]) + triangle[i][j];
// dp[1][1] = dp[0][1], dp[0][0] 중에 max + triangle[1][1] (8) -- 1
// dp[2][1] = dp[1][1], dp[1][0] 중에 max + triangle[2][1] (1)
// dp[2][2] = dp[1][2], dp[1][1] 중에 max + triangle[2][2] (0) -- 2
}
/* 맨 오른쪽 */
// dp[i][i] = dp[i - 1][i - 1] + triangle[i][i];
// dp[1][1] (15) = dp[0][0] (7) + triangle[1][1] (8) -- 1
// dp[2][2] (15) = dp[1][1] (중간값의 max 값) + triangle[2][2] (1) -- 2
for (int w = 0; w < dp.length; w++) {
for (int k = 0; k < dp.length; k++) {
System.out.print(dp[w][k] + " ");
}
System.out.println();
}
System.out.println("------------------------------");
}
for (int i = 0; i < triangle.length; i++) {
answer = Math.max(answer, dp[triangle.length - 1][i]);
}
return answer;
}
* 위 DP 문제에는 중간 값 구할 때 맨 오른쪽 값도 같이 구해지기 때문에 맨 오른쪽 구하는 경우를 주석 처리 하였습니다.
- 분할 정복 : 큰 문제를 해결하기 어려워 작은 문제로 나누어 푸는 방법 ( 작은 문제에서 반복은 없음 )
- 동적 프로그래밍 : 작은 문제들이 반복되는 것 ( 답은 바뀌지 않음 )
Dynamic Programming 방법
모든 작은 문제들은 한번만 풀어야 합니다.
정답을 구한 작은 문제를 어딘가에 메모해놓고, 다시 그보다 큰 문제를 풀어나갈 때 똑같은 작은 문제가 나타나면
메모한 작은 문제의 결과값을 이용합니다.
Dynamic Programming 조건
1) 작은 문제가 반복일 일어날 경우
2) 같은 문제는 구할 때마다 정답이 같음
Memoization ( 메모이제이션 )
작은 문제들이 반복되고, 이 작은 문제들의 결과값은 항상 같다는 점을 이용해 한번 계산한 작은 문제를 저장해놓고 다시 사용하는 것을 의미합니다. 피보나치로 예를 들 수 있습니다.
[ 피보나치 수열 예제 ]
피보나치는 1, 1, 2, 3, 5, 8 .. 의 수를 이루게 됩니다. 재귀로 풀게 될 경우 했던 작업을 또 해야하므로 일정 수 이상의 순열을 구하기가 어렵습니다.
- 점화식 : [이전 수열] + [두 단계 전 수열의 합]
- DP 조건이 해당되는지 확인 1) 작은 문제들이 반복된다. : F(5) = F(4) + F(3) , F(4) = F(3) + F(2) 로 F(3) 이라는 수열이 F(4), F(5) 에서 반복됩니다. 2) 같은 문제를 구할때 마다 정답이 같다. : 첫번째, 두번째 수열은 각각 1로 고정되어 있습니다. 즉, 3번째 수열은 항상 결과가 2입니다. 그리고 4번째 수열은 3번째와 2번째
수열을 이용해 구하므로 언제나 정답이 같습니다.
public class fibonacci {
static long[] memo;
public static int fibonacci_rec(int n) { // 재귀
if (n <= 1) {
return n;
} else {
return fibonacci_rec(n-1) + fibonacci_rec(n-2);
}
}
public static long fibonacci_memoization(int n) { // 메모이제이션
if (n <= 1) {
return n;
}
else if(memo[n] != 0){
return memo[n];
}
else {
return memo[n] = fibonacci_memoization(n - 1) + fibonacci_memoization(n - 2);
}
}
public static void main(String[] args) {
memo = new long[10];
System.out.println(fibonacci_rec(10));
System.out.println(fibonacci_memoization(10));
}
}
[ 재귀와의 차이점 ]
기존 재귀함수와는 다르게 이미 계산된 값을 Memo 라는 배열에 저장하여 사용한다는 차이점이 있습니다.
수많은 마라톤 선수들이 마라톤에 참여하였습니다. 단 한 명의 선수를 제외하고는 모든 선수가 마라톤을 완주하였습니다.
마라톤에 참여한 선수들의 이름이 담긴 배열 participant와 완주한 선수들의 이름이 담긴 배열 completion이 주어질 때, 완주하지 못한 선수의 이름을 return 하도록 solution 함수를 작성해주세요.
제한사항
마라톤 경기에 참여한 선수의 수는 1명 이상 100,000명 이하입니다.
completion의 길이는 participant의 길이보다 1 작습니다.
참가자의 이름은 1개 이상 20개 이하의 알파벳 소문자로 이루어져 있습니다.
참가자 중에는 동명이인이 있을 수 있습니다.
import static java.util.Arrays.sort;
class Solution {
/**
- 문제
마라톤 선수 이름이 담긴 배열 participant
완주한 선수 이름이 담긴 배열 completion
완주하지 못한 선수의 이름을 return 하는 함수 작성
- 제한 사항
1) 참여자수 : 1~100,000
2) completion == participant + 1
3) 참가자 이름
- 알파벳 소문자 20글자 이하
- 동명이인 있을 수 있음
*/
public String solution(String[] participant, String[] completion) {
String result = "";
sort(participant);
sort(completion);
for (int i = 0; i < participant.length-1; i++) {
if(!participant[i].equals(completion[i])){
return participant[i];
}
}
result = participant[participant.length-1];
return result;
}
}
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/~~?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=??
spring.datasource.password=??
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mysql 사용
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.generate-ddl=false
# 로깅 레벨
logging.level.org.hibernate=info
# 하이버네이트가 실행한 모든 SQL문을 콘솔로 출력
spring.jpa.properties.hibernate.show_sql=true
# SQL문을 가독성 있게 표현
spring.jpa.properties.hibernate.format_sql=true
# 디버깅 정보 출력
spring.jpa.properties.hibernate.use_sql_comments=true
두번째 오류
Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set
[ 해결방법 ]
두 오류를 종합해봤을 때, application.properties 의 MySQL 설정을 읽지 못하고 있는 것으로 보입니다.
그 이유는 제가 META-INF 에 PersistenceUnit 을 설정하기위해 persistence.xml 을 만들어 놓고
server.address=localhost
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/TEST_DB?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=test_user
spring.datasource.password=admin
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mysql 사용
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
# 로깅 레벨
logging.level.org.hibernate=info
# 하이버네이트가 실행한 모든 SQL문을 콘솔로 출력
spring.jpa.properties.hibernate.show_sql=true
# SQL문을 가독성 있게 표현
spring.jpa.properties.hibernate.format_sql=true
# 디버깅 정보 출력
spring.jpa.properties.hibernate.use_sql_comments=true
[서버에서 클라이언트 쪽으로 전송하는 응답 데이터 ] 의 형식으로 클라이언트와 서버 간에 데이터 전송이 이뤄집니다.
DTO 를 왜 사용하는가?
1. 코드의 간결성
ResponseEntity 를 통해 데이터를 저장하는 곳에는 @RequestParam 애노테이션이 사용됩니다.
그런데 프로젝트의 규모가 커질 수록 RequestParam 의 개수는 늘어날 수 밖에 없습니다.
만약, 클라이언트와 요청 데이터를 하나의 객체로 모두 전달 받을 수 있다면 코드 자체가 아주 간결해질 것입니다.
DTO 클래스가 바로 요청 데이터를 하나의 객체로 전달 받는 역할을 해줍니다.
2. 데이터 유효성 검증의 단순화
이메일이 유효한 입력 값인지 확인하려면 email.matches("^[~~$") 와 같이 복잡한 정규 표현식으로 코드를 작성하게 됩니다.
하지만 HTTP 요청을 전달 받는 핸들러 메서드는 요청을 받는 것이 주목적이기 때문에 최대한 간결하게 작성되는 것이 좋습니다.
그래서 DTO 클래스에 이러한 유효성 검증을 작성하면 핸들러 메서드에는 아주 간결하게 표현될 수 있습니다.
DTO 클래스를 적용하기 위한 코드 리팩토링 절차
회원 정보를 전달 받을 DTO 클래스를 생성합니다.
컨트롤러에서 현재 회원 정보로 전달 받는 각 데이터 항복을 DTO 클래스의 멤버 변수로 추가해줍니다.
클라이언트 쪽에서 전달하는 요청 데이터를 @RequestParam 애너테이션으로 전달 받는 핸들러 메서드를 찾습니다.
RequestBody 가 필요한 핸들러는 POST, PATCH, PUT 과 같이 리소스 수정/추가 발생할 때 입니다.
GET 은 조회하는 용도이기 때문에 Body 는 필요 없습니다.
@RequestParam 코드를 DTO 클래스의 객체로 수정합니다.
Map 객체로 JSON 을 받아오는 객체 였다면 Reponse Body 를 DTO 클래스 객체로 변경해줍니다.
DTO 클래스에 유효성 검증 적용하기
org.springframework.boot:spring-boot-starter-validation 이라는 디펜던시 항목을 추가해줘야 합니다.
[ DTO 내부 멤버에서 검증 애노테이션 ]
@NotBlank : 정보가 비어있지 않은지를 검증
@Email : 이메일 주소인지 검증
@Pattern : 정규표현식에 매치되는 유효한 번호인지 검증
[ 유효성 검증 시작 ]
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
}
...
...
}
@Valid 애노테이션을 추가해줍니다.
[ @PathVariable 이 추가된 유효성 검증하기 ]
@RestController
@RequestMapping("/v1/members")
@Validated // (1)
public class MemberController {
...
...
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(@PathVariable("member-id") @Min(1) long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
// No need Business logic
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
}
- @Min(1) : memberId 가 1 이상의 숫자일 경우에만 검증에 통과하도록 해줍니다.
- @Validated : @PathVariable 이 추가된 변수에 유효성 검증이 정상적으로 수행되려면 해당 애노테이션이 필수입니다.
Spring MVC 에서 특정 클래스에 @RestController 를 추가하면 해당 클래스가 REST API 리소스를 처리하기 위한 API 엔드포인트로 동작함을 정의합니다.
그리고 해당 애노테이션이 추가된 클래스는 애플리케이션 로딩 시, Spring Bean 으로 등록해줍니다.
@RequestMapping
클라이언트의 요청과 그 요청을 처리하는 핸들러 메서드를 매핑해주는 역할을 합니다.
예를 들어, RequsetMapping("v1/member") 을 하게 된다면 괄호 안에 있는 코드는 Controller 클래스 레벨에 추가하여 클래스 전체에 사용되는 공통 URL 설정을 하게 됩니다. 이러한 메서드를 핸들러 메서드라고 부릅니다.
@RequestMapping(value, produces)
[produces] 해당 속성은 응답 데이터를 어떤 미디어 타입으로 클라이언트에게 전송할 지를 설정합니다. JSON 형식의 데이터를 응답 데이터로 전송하겠다는 의미로는 MediaType.APPLICATION_JSON_VALUE 로 값을 설정할 수 있습니다. 이 설정을 하지않는다면 JSON 형식이 아닌, 문자열 자체를 전송하게 됩니다.
@PostMapping
클라이언트 요청 데이터를 서버에 생성할 때 사용하는 애너테이션입니다.
주로 회원 정보를 등록해주는 역할을 해줍니다.
@RequsetParam
핸들러 메서드의 피라미터 종류 중 하나입니다.
주로 클라이언트쪽에서 전송하는 요청 데이터를 [ 쿼리 피라미터, 폼 데이터, x-www-form-urlencoded 형식 ] 으로 전송하면
HTTP 메시지의 구성 요소 중 하나로써 클라이언트 요청이나 서버의 응답에 포함되어 부가적인 정보를 HTTP 메시지에 포함할 수 있도록 해줍니다.
[ 사용 목적 ]
클라이언트와 서버 관점에서의 대표적인 HTTP 헤더 예시
클라이언트와 서버 관점에서 내부적으로 가장 많이 사용되는 헤더 정보로는 "Content-Type" 이 있습니다.
클라이언트와 서버가 HTTP 메시지 바디의 데이터 형식이 무엇인지를 알려주는 역할을 합니다.
그래서 클라이언트와 서버는 Content-Type 이 명시된 데이터 형식에 맞는 데이터를 주고 받는 것입니다.
개발자들이 직접 실무에서 사용하는 대표적인 HTTP 헤더 예시
사실 개발자들이 직접 HTTP 헤더를 건드릴 일은 없습니다.
하지만 코드 레벨에서 컨트롤해야 하는 경우가 있는데 이런 경우의 대표적인 예시 두가지를 보겠습니다.
1. Authorization
이는 클라이언트가 적절한 자격 증명을 가지고 있는지를 확인하기 위한 정보입니다.
일반적으로 REST API 기반 애플리케이션의 경우 클라이언트와 서버간의 로그인 인증에 통과한 클라이언트들은
"Authorization" 헤더 정보를 기준으로 인증에 통과한 클라이언트가 맞는지 확인하는 절차를 가집니다.
2. User-Agent
여러 유형의 클라이언트가 하나의 서버 애플리케이션에 요청을 전송하는 경우가 많습니다.
어떤 사용자는 데스크탑 웹 브라우저에서 서버에 요청을 보내고, 또 다른 사람은 스마트폰에서 요청을 보냅니다.
이런 경우 들어오는 요청을 구분해서 응답 데이터를 다르게 보내줘야 되는 경우가 있습니다.
예를 들어, 데스크탑 브라우저에서는 더 큰 화면인만큼 더 많은 정보를 보여줍니다.
이 경우, "User-Agent" 정보를 통해서 구분할 수 있습니다.
HTTP Request 헤더 정보 얻기
@RequestHeader("user-agent") : 특정 헤더 정보만 읽는 예제입니다.
HttpServletRequest 객체 : Request 헤더 정보에 다양한 방법으로 접근이 가능합니다.
HttpEntity객체 : Request 헤더와 바디 정보를 래핑하고 있으며, 조금 더 쉽게 헤더와 바디에 접근할 수 있습니다.
* HttpEntity 객체
Entry 를 통해 각각의 헤더 정보에 접근할 수 있는데, 특이한 것은 자주 사용될만한 헤더 정보들은 get() 으로 가져올 수 있습니다.
HTTP Response 헤더 정보 추가
ResponseEntity 와 HttpHeaders 를 이용해 헤더 정보 추가하기
@RestController
@RequestMapping(path = "/v1/members")
public class MemberController{
@PostMapping
public ResponseEntity postMember(@RequestParam("email") String email,
@RequestParam("name") String name,
@RequestParam("phone") String phone) {
// (1) 위치 정보를 헤더에 추가
HttpHeaders headers = new HttpHeaders();
headers.set("Client-Geo-Location", "Korea,Seoul");
return new ResponseEntity<>(new Member(email, name, phone), headers,
HttpStatus.CREATED);
}
}
RestClient
REST API 서버에 HTTP 요청을 보낼 수 있는 클라이언트 툴 또는 라이브러리를 의미합니다.
Postman 은 UI 가 갖춰진 RestClient 라고 볼 수 있습니다.
- Client : 서버쪽의 리소스를 이용하는 쪽
* 그래서 [ 어떤 서버가 HTTP 통신을 통해서 다른 서버의 리소스를 이용한다면 그 때만큼은 클라이언트의 역할을 합니다. ]
위의 그림에서는 웹 브라우저가 클라이언트가 됩니다.
RestTemplate
Java 에서 사용할 수 있는 다양한 HTTP Client 라이브러리가 있습니다. ( HttpURLConnection, OkHttp 3, Netty 등 )
Spring 에서는 이 HTTP Client 라이브러리 중 하나를 이용해 다른 백엔스 서버에 HTTP 요청을 보낼 수 있는 REST Client API 를 제공하는데, 이를 RestTemplate 라고 합니다.
이 템플릿을 이용하면 Rest EndPoint 지정, 헤더 설정, 피라미터 및 바디 설정을 한 줄의 코드로 쉽게 할 수 있습니다.
[ URI 생성 ]
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
public class RestClientExample01 {
public static void main(String[] args) {
// (1) 객체 생성
RestTemplate restTemplate =
new RestTemplate(new HttpComponentsClientHttpRequestFactory());
// (2) URI 생성
UriComponents uriComponents =
UriComponentsBuilder
.newInstance()
.scheme("http")
.host("worldtimeapi.org")
// .port(80)
.path("/api/timezone/{continents}/{city}")
.encode()
.build();
URI uri = uriComponents.expand("Asia", "Seoul").toUri();
}
}
[ 설명 ] newInstance(): UriComponentsBuilder 객체를 생성합니다. scheme(): URI의 scheme을 설정합니다. host(): 호스트 정보를 입력합니다. port(): 디폴트 값은 80이므로 80 포트를 사용하는 호스트라면 생략 가능합니다. path(): URI의 경로(path)를 입력합니다. URI의 path에서 {continents}, {city} 의 두 개의 템플릿 변수를 사용하고 있습니다. 두 개의 템플릿 변수는 uriComponents.expand("Asia", "Seoul").toUri(); 에서 expand() 메서드 파라미터의 문자열로 채 워집니다. 즉, 빌드 타임에 {continents}는 ‘Asia’, {city}는 ‘Seoul’로 변환됩니다. encode(): URI에 사용된 템플릿 변수들을 인코딩 해줍니다.여기서 인코딩의 의미는 non-ASCII 문자와 URI에 적절하지 않은 문자를 Percent Encoding 한다는 의미입니다. build(): UriComponents 객체를 생성합니다. expand(): 파라미터로 입력한 값을 URI 템플릿 변수의 값으로 대체합니다. toUri(): URI 객체를 생성합니다.
[ 요청 전송 ]
public class RestClientExample01 {
public static void main(String[] args) {
// (1) 객체 생성
RestTemplate restTemplate =
new RestTemplate(new HttpComponentsClientHttpRequestFactory());
// (2) URI 생성
UriComponents uriComponents =
UriComponentsBuilder
.newInstance()
.scheme("http")
.host("worldtimeapi.org")
// .port(80)
.path("/api/timezone/{continents}/{city}")
.encode()
.build();
URI uri = uriComponents.expand("Asia", "Seoul").toUri();
// (3) Request 전송
String result = restTemplate.getForObject(uri, String.class);
System.out.println(result);
}
}
카카오에 입사한 신입 개발자네오는 "카카오계정개발팀"에 배치되어, 카카오 서비스에 가입하는 유저들의 아이디를 생성하는 업무를 담당하게 되었습니다. "네오"에게 주어진 첫 업무는 새로 가입하는 유저들이 카카오 아이디 규칙에 맞지 않는 아이디를 입력했을 때, 입력된 아이디와 유사하면서 규칙에 맞는 아이디를 추천해주는 프로그램을 개발하는 것입니다. 다음은 카카오 아이디의 규칙입니다.
아이디의 길이는 3자 이상 15자 이하여야 합니다.
아이디는 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.) 문자만 사용할 수 있습니다.
단, 마침표(.)는 처음과 끝에 사용할 수 없으며 또한 연속으로 사용할 수 없습니다.
"네오"는 다음과 같이 7단계의 순차적인 처리 과정을 통해 신규 유저가 입력한 아이디가 카카오 아이디 규칙에 맞는 지 검사하고 규칙에 맞지 않은 경우 규칙에 맞는 새로운 아이디를 추천해 주려고 합니다. 신규 유저가 입력한 아이디가new_id라고 한다면,
1단계 new_id의 모든 대문자를 대응되는 소문자로 치환합니다.
2단계 new_id에서 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거합니다.
3단계 new_id에서 마침표(.)가 2번 이상 연속된 부분을 하나의 마침표(.)로 치환합니다.
4단계 new_id에서 마침표(.)가 처음이나 끝에 위치한다면 제거합니다.
5단계 new_id가 빈 문자열이라면, new_id에 "a"를 대입합니다.
6단계 new_id의 길이가 16자 이상이면, new_id의 첫 15개의 문자를 제외한 나머지 문자들을 모두 제거합니다.
만약 제거 후 마침표(.)가 new_id의 끝에 위치한다면 끝에 위치한 마침표(.) 문자를 제거합니다.
7단계 new_id의 길이가 2자 이하라면, new_id의 마지막 문자를 new_id의 길이가 3이 될 때까지 반복해서 끝에 붙입니다.
예를 들어, new_id 값이 "...!@BaT#*..y.abcdefghijklm" 라면, 위 7단계를 거치고 나면 new_id는 아래와 같이 변경됩니다.
2단계 '!', '@', '#', '*' 문자가 제거되었습니다. "...!@bat#*..y.abcdefghijklm"→"...bat..y.abcdefghijklm"
3단계 '...'와 '..' 가 '.'로 바뀌었습니다. "...bat..y.abcdefghijklm"→".bat.y.abcdefghijklm"
4단계 아이디의 처음에 위치한 '.'가 제거되었습니다. ".bat.y.abcdefghijklm"→"bat.y.abcdefghijklm"
5단계 아이디가 빈 문자열이 아니므로 변화가 없습니다. "bat.y.abcdefghijklm"→"bat.y.abcdefghijklm"
6단계 아이디의 길이가 16자 이상이므로, 처음 15자를 제외한 나머지 문자들이 제거되었습니다. "bat.y.abcdefghijklm"→"bat.y.abcdefghi"
7단계 아이디의 길이가 2자 이하가 아니므로 변화가 없습니다. "bat.y.abcdefghi"→"bat.y.abcdefghi"
따라서 신규 유저가 입력한 new_id가 "...!@BaT#*..y.abcdefghijklm"일 때, 네오의 프로그램이 추천하는 새로운 아이디는 "bat.y.abcdefghi" 입니다.
[문제]
신규 유저가 입력한 아이디를 나타내는 new_id가 매개변수로 주어질 때, "네오"가 설계한 7단계의 처리 과정을 거친 후의 추천 아이디를 return 하도록 solution 함수를 완성해 주세요.
[제한사항]
new_id는 길이 1 이상 1,000 이하인 문자열입니다. new_id는 알파벳 대문자, 알파벳 소문자, 숫자, 특수문자로 구성되어 있습니다. new_id에 나타날 수 있는 특수문자는-_.~!@#$%^&*()=+[{]}:?,<>/로 한정됩니다.
[입출력 예]nonew_idresult
예1
"...!@BaT#*..y.abcdefghijklm"
"bat.y.abcdefghi"
예2
"z-+.^."
"z--"
예3
"=.="
"aaa"
예4
"123_.def"
"123_.def"
예5
"abcdefghijklmn.p"
"abcdefghijklmn"
입출력 예에 대한 설명
입출력 예 #1 문제의 예시와 같습니다.
입출력 예 #2 7단계를 거치는 동안 new_id가 변화하는 과정은 아래와 같습니다.
1단계 변화 없습니다. 2단계"z-+.^."→"z-.." 3단계"z-.."→"z-." 4단계"z-."→"z-" 5단계 변화 없습니다. 6단계 변화 없습니다. 7단계"z-"→"z--"
입출력 예 #3 7단계를 거치는 동안 new_id가 변화하는 과정은 아래와 같습니다.
1단계 변화 없습니다. 2단계"=.="→"." 3단계 변화 없습니다. 4단계"."→""(new_id가 빈 문자열이 되었습니다.) 5단계""→"a" 6단계 변화 없습니다. 7단계"a"→"aaa"
입출력 예 #4 1단계에서 7단계까지 거치는 동안 new_id("123_.def")는 변하지 않습니다. 즉, new_id가 처음부터 카카오의 아이디 규칙에 맞습니다.
입출력 예 #5 1단계 변화 없습니다. 2단계 변화 없습니다. 3단계 변화 없습니다. 4단계 변화 없습니다. 5단계 변화 없습니다. 6단계"abcdefghijklmn.p"→"abcdefghijklmn."→"abcdefghijklmn" 7단계 변화 없습니다.
[ 내가 작성한 코드 ]
import java.util.Locale;
public class 신규아이디추천 {
/*
- 해야할 일
1) 규칙에 맞지 않는 아이디를 입력했을 때, 입력된 아이디와 유사하면서 규칙에 맞는 아이디를 추천
- 아이디 규칙
1) 길이: 3 ~ 15
2) 사용가능 문자 : 소문자, 빼기, 밑줄, 마침표
3) 단, 마침표는 시작과 끝에 사용 불가능, 연속으로 사용 불가능
- 해야할 일의 처리 과정
1단계 new_id의 모든 대문자를 대응되는 소문자로 치환합니다.
2단계 new_id에서 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거합니다.
3단계 new_id에서 마침표(.)가 2번 이상 연속된 부분을 하나의 마침표(.)로 치환합니다.
4단계 new_id에서 마침표(.)가 처음이나 끝에 위치한다면 제거합니다.
5단계 new_id가 빈 문자열이라면, new_id에 "a"를 대입합니다.
6단계 new_id의 길이가 16자 이상이면, new_id의 첫 15개의 문자를 제외한 나머지 문자들을 모두 제거합니다.
만약 제거 후 마침표(.)가 new_id의 끝에 위치한다면 끝에 위치한 마침표(.) 문자를 제거합니다.
7단계 new_id의 길이가 2자 이하라면, new_id의 마지막 문자를 new_id의 길이가 3이 될 때까지 반복해서 끝에 붙입니다.
능 */
public static void main(String[] args) {
String newId = "00.@cdefgTWhijklm...!_I'," ;
String solution = solution(newId);
System.out.println("res: " + solution);
}
public static String solution(String new_id) {
String answer = "";
String one = "";
String two = "";
String three = "";
String four = "";
String five = "";
String six = "";
String seven = "";
// 1
one = new_id.toLowerCase(Locale.ROOT);
char[] ch = one.toCharArray();
// 2
for (int i = 0; i < ch.length; i++) {
if(('a'<= ch[i] && ch[i] <= 'z') || ('0' <= ch[i] && ch[i] <= '9') ||
(ch[i]=='-') || (ch[i] =='_') || (ch[i] == '.')){
two += ch[i];
} else {
continue;
}
}
// 3
three = two;
while(three.contains("..")){
three = three.replace("..", ".");
}
//4
four = three;
if (four.length() > 0 && four.charAt(0) == '.') {
four = four.substring(1, four.length());
}
if (four.length() > 0 && four.charAt(four.length() - 1) == '.') {
four = four.substring(0, four.length() - 1);
}
//5
five = four;
if (five.isEmpty()) {
five = "a";
}
//6
six = five;
if(six.length() > 15){
six = six.substring(0, 15);
if(six.substring(six.length()-1,six.length()).equals(".")){
six = six.substring(0,six.length()-1);
}
}
//7
seven = six;
while(seven.length()<=2){
ch = seven.toCharArray();
seven += ch[ch.length-1];
}
answer = seven;
return answer;
}
}
저는 위와 같이 1단계부터 7단계까지 각 단계별로 풀이를 하였습니다.
하지만 다른 사람의 풀이를 보니 replaceAll 과 정규표현식의 중요성을 알 수 있었습니다.
PUT : 결과를 대체하는 메서드이므로, 몇번 하든 결과가 같다 ( 멱등 ) GET : 한번 조회하든, 두번 조회하든 결과가 조회된다. ( 멱등 ) DELETE : 결과를 삭제한다. 같은 요청을 여러번해도 삭제된 결과는 같다 ( 멱등 ) POST : 두 번 호출하면 결과가 중복될 수 있다. ( 멱등이 아니다 !! )
멱등 활용하기
자동 복구 메커니즘
서버가 TIMEOUT 등으로 정상 응답을 주지 못할 때, 클라이언트가 같은 요청을 다시 해도 되는가의 판단 근거가 됩니다.
멱등은 중간에 리소스를 변경하는 것도 고려를 하는가?
GET -> PUT -> GET 으로 데이터가 바뀐것을 조회할 수 있습니다.
멱등은 외부 요인으로 중간에 리소스가 변경되는 것은 고려하지 않습니다.
캐시 가능
응답 결과 리소스를 캐시해서 사용해도 되는가?
GET, HEAD, POST, PATCH 캐시 가능
실제로는 GET, HEAD 정도만 캐시로 사용되며, POST,PATCH 는 본문 내용까지 캐시 키로 고려해야하는데, 구현이 쉽지 않음
클라이언트에서 서버로 데이터 전송하는 방법
쿼리 파라미터를 통한 데이터 전송
URI 끝에 쿼리 파라미터를 넣어서 전송하는 방법입니다.
이것은 주로 GET 으로 정렬 필터 ( 검색어 ) 에 사용을 많이 하는 편입니다.
메시지 바디를 통한 데이터 전송
HTTP 메세지 바디를 통해서 전송하는데, POST/PUT/PATCH 를 이용해서
회원가입, 상품주문, 리소스 등록, 리소스 변경할 때 주로 사용합니다.
클라이언트에서 서버로 데이터를 전송하는 상황 4가지
정적 데이터 조회
- 이미지, 정적텍스트 문서
- 조희는 GET 사용
- 정적 데이터는 일반적으로 쿼리 피라미터 없이 리소스 경로로 단순하게 조회 가능
동적 데이터 조회
- 주로 검색, 게시판 목록에서 정렬필터 ( 검색어 )
- 조회 조건을 줄여주는 필터, 조회 결과를 정렬하는 정렬 조건에 주로 사용
- 조회는 GET 사용
- GET 은 쿼리 피라미터 사용해서 데이터 전달
클라이언트 서버
HTML Form 을 통한 데이터 전송
- 회원가입, 상품 주문, 데이터 변경
- HTML Form submit 시 POST 전송
- 회원 가입, 상품 주문, 데이터 변경에 주로 사용됨
- Content-Type : application/x-www-form-urlencoded 사용
-> form 내용을 메세지 바디에 넣어줘서 전송 ( 쿼리 피라미터 형식 )
-> 전송 데이터를 url encoding 처리
- GET 은 메세지 바디를 쓰지않고, 쿼리 파라미터에 넣어서 서버에 전달합니다.
- 그러나 위의 예제에서는 저장하는 save 메서드에 GET ( 조회 ) 메서드를 사용하므로 쓰면 안되는 문장입니다.
- 위와 같이 멤버를 조회하는 문장에는 사용할 수 있습니다.
- 파일 업로드 같은 바이너리 데이터 전송시 사용
- 이름이 multipart 인 이유는 다른 종류의 여러 파일과 폼의 내용 함께 전송 가능하기 때문입니다.
*** HTML Form 전송은 GET,POST 만 지원
HTTP API 를 통한 데이터 전송
ex. 안드로이드 앱 애플리케이션에서 클라이언트에서 서버로 데이터를 바로 전송해줘야 할 때,
이를 HTTP API 로 전송한다고 부릅니다. ( 그냥 다 만들어서 넘기는 것입니다. )
- 서버에서 서버로 백엔드 시스템 통신
- 앱 클라이언트 ( 아이폰, 안드로이드 )
- 웹 클라이언트
-> HTML 에서 Form 전송 대신 자바 스크립트를 통한 통신에 사용 ( AJAX )
-> React, Vue.js 같은 웹 클라이언트와 API 통신
- POST, PUT, PATCH : 메세지 바디를 통해 데이터 전송
- GET : 조회,쿼리 피라미터로 데이터 전달
- Content-Type : application/json 을 주로 사용 ( TEXT, XML, JSON 등등 )
function add(x,y){
return x+y;
}
function mul(x,y){
return x*y;
}
function cal(a,b){
return a(10,10) + b(10,10); // a라는 함수는 선언된적 없음
}
cal(mul,mul); // cal 이 호출되면서 a라는 함수에 각 함수들이 할당됨
화살표 함수
함수의 네이밍이 없고, 인자와 함수 본문만 보여진다.
형태 :
(a,b) => {
let z = 10
let res = z + a + b
return res
};
만약, 화살표 함수를 콜백함수로 사용한다면?
네이밍을 안해도됨
다른 곳에서 사용할 수 없음
콜백 지옥에 빠질 수 있음
function cal(a,b){
return a(10,10) + b(10,10);
}
cal((a,b) => a+b, (a+b) => a*b);
// cal 이 호출됨과 동시에 화살표 함수로 함수가 선언되어 실행됨
동기와 비동기
js는 일을 처리할 수 있는 스레드가 1개 싱글 쓰레드라고 합니다.
모든 일을 여러명이 처리할 수 없다면 항상 기다려야하는 문제가 발생하고, 무한대기에 빠질 수 있습니다.
* 동기와 비동기를 이해하려면 자바시크립트 실행 동작원리를 이해해야함 ( 숙제 )
DOM
메소드
getElementById() : 해당하는 Id 를 가진 요소에 접근하기
querySelector() : css 선택자로 단일 요소에 접근하기
document.createElement (target) : 타겟 요소를 생성합니다.
element.appendChild ( target ) : 타겟 요소를 요소의 자식으로 위치합니다.