1. 오늘의 한 줄 요약 Spring Boot 기반 MSA에서 사용자·카탈로그·주문 서비스를 각각 구성하고, REST API와 JPA로 데이터를 처리하는 방법, Spring Security로 인증을 적용하는 방법, API 게이트웨이를 통한 호출 흐름을 학습했다. 2. 배운 내용 핵심 개념: - 서비스별 역할과 독립적인 데이터 관리 서비스 경계 : 사용자 서비스는 회원 가입, 사용자 조회, 로그인 기능을 맡는다. 카탈로그 서비스는 상품 목록과 재고 정보를 관리한다. 주문 서비스는 주문 생성과 사용자별 주문 내역 조회를 맡는다. 각 서비스는 독립적인 데이터베이스를 사용한다. 실습에서는 가볍게 실행할 수 있는 H2를 사용하며, 인메모리 방식에서는 애플리케이션 종료 후 데이터가 사라진다. 지원 구성 요소 : 유레카는 실행 중인 서비스의 위치를 등록·조회하게 하고, API 게이트웨이는 클라이언트 요청을 서비스로 전달한다. Kafka를 통한 주문 이벤트 전달, Config Server를 통한 설정 관리, 모니터링과 배포 자동화는 서비스가 늘어날 때 필요한 운영 요소로 소개되었다. - 사용자 서비스의 REST API와 데이터 흐름 일관된 API 설계 : GET /users 는 전체 사용자 조회, GET /users/{userId} 는 특정 사용자 조회, POST /users 는 회원 가입에 사용한다. 게이트웨이를 통하면 /user-service/users 처럼 서비스 식별 경로가 앞에 붙는다. 게이트웨이는 이 경로를 이용해 요청을 전달하고, 필요하면 실제 서비스의 경로에 맞게 변경한다. 리소스 이름은 /users 처럼 명사형 복수형을 사용하고, 조회와 생성은 HTTP 메서드로 구분한다. 회원 가입 데이터 처리 : @RequestBody 가 JSON 요청을 RequestUser 로 받고, @NotNull , @Size , @Email 등으로 입력값을 검증한다. 요청 객체의 값을 DTO로 옮겨 사용자 ID를 생성하고 비밀번호를 해시한 뒤, 엔티티로 변환해 JPA 레포지토리에 저장한다. 저장 결과는 응답 객체로 변환한다. 응답에는 사용자 ID 등 필요한 정보만 담고 비밀번호는 제외한다. ResponseEntity 로 상태 코드·헤더·본문을 지정할 수 있으며, 새 사용자를 생성한 결과에는 201 Created 가 적절하다. JPA 활용 : 엔티티는 데이터베이스 테이블과 매핑하고, CrudRepository 또는 JpaRepository 를 상속한 레포지토리에서 save() , findAll() , findById() 등을 사용한다. findByEmail() , findByUserId() 처럼 메서드 이름을 이용한 조회 메서드도 선언할 수 있다. - Spring Security의 인증·인가와 비밀번호 처리 인증과 인가 : 인증은 사용자가 누구인지 확인하는 과정이고, 인가는 확인된 사용자가 특정 기능에 접근할 수 있는지 판단하는 과정이다. SecurityFilterChain 에서 회원 가입이나 H2 콘솔처럼 공개해야 하는 경로는 permitAll() 로 허용하고, 보호할 경로는 authenticated() 로 설정한다. 401 Unauthorized 는 인증이 필요한 경우, 403 Forbidden 은 인증은 되었지만 권한이 부족한 경우를 구분하는 데 쓰인다. 비밀번호 해시 : 비밀번호를 평문으로 저장하지 않고 BCryptPasswordEncoder.encode() 로 해시한다. 로그인할 때는 저장된 값을 원래 비밀번호로 되돌리는 것이 아니라 matches() 로 입력값과 해시의 일치 여부를 검사한다. - 카탈로그·주문 서비스와 서비스 간 연동 상태 카탈로그 서비스 : GET /catalog-service/catalogs 로 상품 목록을 조회한다. 실습 설정에서는 data.sql 에 초기 상품 세 개를 넣고, 테이블 생성이 끝난 다음 데이터가 삽입되도록 초기화 순서를 맞춘다. 조회된 엔티티는 응답 객체로 바꾸어 200 OK 와 함께 반환한다. 주문 서비스 : POST /order-service/{userId}/orders 는 주문을 생성하고 201 Created 를 반환한다. GET /order-service/{userId}/orders 는 해당 사용자의 주문 내역을 조회하고 200 OK 를 반환한다. 주문 ID는 UUID로 만들고, 주문 총액은 수량과 단가를 곱해 계산한다. 현재 연동 범위 : 사용자 서비스의 상세 응답에는 orders 필드가 있지만, 사용자 서비스가 주문 서비스를 호출하는 기능은 아직 구현되지 않았다. 따라서 사용자 상세 조회의 orders 는 빈 리스트로 표시된다. 주문 내역 자체는 주문 서비스의 조회 API로 확인할 수 있다. 두 서비스의 데이터를 합치는 작업은 이후 단계다. 새롭게 알게 된 점 : 응답 객체에 orders 필드를 추가하는 것과 실제 주문 서비스에서 데이터를 가져오는 것은 별개의 작업이다. 서비스 간 호출이 연결되기 전까지는 필드만으로 주문 내역이 채워지지 않는다. 3. 실습 / 적용 직접 해본 것: 학습 내용의 실습 순서를 사용자·카탈로그·주문 서비스 설정 -> 유레카 등록 -> 게이트웨이 라우팅 -> API 호출 -> 각 H2 데이터 확인으로 정리했다. 개인 환경에서 직접 실행했다는 기록은 없어, 아래 결과는 제시된 테스트 흐름을 기준으로 적었다. 회원 가입 요청에는 이름·이메일·비밀번호를 JSON으로 보내고, 응답에 비밀번호가 포함되지 않는지 확인한다. 카탈로그 조회로 초기 상품 데이터를 확인한 뒤, 주문 생성과 사용자별 주문 조회를 순서대로 호출한다. 결과: 회원 가입과 주문 생성은 새 리소스를 만들므로 201 Created , 목록 및 상세 조회는 200 OK 로 응답하도록 구성된다. 주문 서비스의 사용자별 조회에서는 주문 내역을 확인할 수 있지만, 사용자 서비스의 상세 응답에 있는 orders 는 아직 빈 리스트다. 4. 문제와 해결 막힌 부분: Spring Security를 추가하면 회원 가입처럼 로그인 전에 사용해야 하는 경로까지 인증이 필요해질 수 있다. 사용자 상세 조회에서 orders 필드를 만들었는데도 주문 내역이 보이지 않는다. 해결 방법: SecurityFilterChain 에서 회원 가입과 필요한 공개 경로를 permitAll() 로 지정하고, 인증이 필요한 경로만 보호한다. 비밀번호는 BCrypt로 해시해 저장한다. 현재는 사용자 서비스가 주문 서비스를 호출하지 않으므로 orders 가 빈 리스트인 것이 예상된 결과다. 다음 단계에서 사용자 서비스의 주문 조회 호출을 구현하고, 주문 서비스 응답을 ResponseUser.orders 에 넣어야 한다. 5. 다음에 할 일 강의 자료 복습 실습코드 복습 github commit/push 하기
문제 링크 https://swexpertacademy.com/main/code/problem/problemDetail.do?contestProbId=AWgv9va6HnkDFAW0 1. 문제 접근 이 문제에서 가장 먼저 파악해야 하는 것은 어떤 값이 고정 되어 있고, 어떤 값이 변하는가 이다. 규영 이가 내는 9장의 카드 순서는 입력으로 주어져 고정 되어 있다. 반면 인영 이는 규영이가 가지지 않은 나머지 9장의 카드를 가지고 있으며, 이 카드들을 내는 순서 에 따라 경기 결과가 달라진다. 예를 들어 인영이의 카드가 3장이라고 단순화하면, [2, 4, 6] 가능한 순서 2 4 6 2 6 4 4 2 6 4 6 2 6 2 4 6 4 2 같은 카드를 사용하더라도 순서 가 달라지면 서로 다른 경우가 된다. 따라서 인영이가 카드를 내는 모든 순서를 만들어 확인해야 한다. 9개의 카드를 순서를 고려 하여 나열한다 → 순열 인영이 카드의 모든 순열은 9! = 362,880 개이므로 완전탐색 으로 확인할 수 있다. 2. 전체 풀이 흐름 문제를 크게 나누면 다음과 같다. 규영이 카드 9장 입력 ↓ 규영이가 가지지 않은 카드 = 인영이 카드 9장 구하기 ↓ DFS로 인영이 카드의 모든 순열 생성 ↓ 9장의 순서가 완성되면 규영이와 인영이 카드 비교 ↓ 9라운드의 점수 계산 ↓ 규영이 승 / 패 횟수 증가 핵심은 인영이 카드의 모든 순서를 DFS로 만드는 것이다. 3. 순열에서 depth와 i 순열을 구현할 때 가장 헷갈렸던 부분은 depth 와 반복문의 i 가 각각 무엇을 의미하는가였다. static void dfs(int depth) { for (int i = 0; i < 9; i++) { if (visited[i]) continue; visited[i] = true; selected[depth] = inYoung[i]; dfs(depth + 1); visited[i] = false; } } 각 변수의 역할은 다음과 같이 생각할 수 있다. depth = 현재 몇 번째 자리를 결정하고 있는가? i = 현재 자리에 어떤 카드를 넣을 것인가? 예를 들어 인영이의 카드가 다음과 같다고 하자. inYoung = [2, 4, 6] depth = 0 이면 첫 번째 자리를 결정하고 있는 것이다. i = 0 → selected[0] = 2 i = 1 → selected[0] = 4 i = 2 → selected[0] = 6 반복문의 i 를 통해 현재 자리에 들어갈 수 있는 모든 카드를 하나씩 선택한다. 4. visited가 필요한 이유 순열 에서는 하나의 카드를 한 번만 사용할 수 있다. 따라서 이미 선택한 카드인지 확인하기 위해 visited 배열을 사용한다. if (visited[i]) continue; 아직 사용하지 않은 카드라면 선택한다. visited[i] = true; selected[depth] = inYoung[i]; 그리고 다음 자리를 결정한다. dfs(depth + 1); 해당 카드를 선택했을 때 발생하는 모든 경우를 확인했다면 다시 사용하지 않은 상태로 되돌린다. visited[i] = false; 전체 흐름은 다음과 같다. 카드 선택 ↓ visited = true ↓ selected[depth]에 저장 ↓ dfs(depth + 1) ↓ visited = false ↓ 다른 카드 선택 이것이 백트래킹의 선택 → 재귀 → 원상복구 구조이다. 5. DFS가 실제로 동작하는 과정 카드가 3장이라고 가정해보자. [2, 4, 6] DFS는 다음과 같이 진행된다. depth = 0 ├── 2 선택 │ │ │ ├── 4 선택 │ │ └── 6 선택 │ │ → [2, 4, 6] │ │ │ └── 6 선택 │ └── 4 선택 │ → [2, 6, 4] │ ├── 4 선택 │ │ │ ├── 2 선택 │ │ └── 6 선택 │ │ → [4, 2, 6] │ │ │ └── 6 선택 │ └── 2 선택 │ → [4, 6, 2] │ └── 6 선택 │ ├── 2 선택 │ └── 4 선택 │ → [6, 2, 4] │ └── 4 선택 └── 2 선택 → [6, 4, 2] 3장의 카드라면 3! = 6개의 순열이 만들어진다. 실제 문제에서는 9장의 카드이므로 9! = 362,880 개의 순열을 탐색한다. 6. 순열이 완성되었는지 확인 depth == 9 가 되었다는 것은 selected[0] ~ selected[8] 까지 모두 채워졌다는 의미이다. 즉, 인영이가 카드를 내는 하나의 순서가 완성된 것이다. if (depth == 9) { // 점수 계산 return; } 예를 들어 selected = [2, 6, 4, 10, 12, 15, 18, 7, 3] 처럼 하나의 순열이 완성되었다면 이제 규영이의 카드와 라운드별로 비교한다. 7. 점수 계산 각 라운드에서는 두 사람이 낸 카드의 숫자를 비교한다. 숫자가 더 큰 사람이 두 카드에 적힌 숫자의 합만큼 점수를 얻는다. int gyuScore = 0; int inScore = 0; for (int i = 0; i < 9; i++) { int sum = gyuYoung[i] + selected[i]; if (gyuYoung[i] > selected[i]) { gyuScore += sum; } else { inScore += sum; } } 9라운드가 끝난 후 최종 점수를 비교한다. if (gyuScore > inScore) { win++; } else { lose++; } 이 과정을 인영이가 카드를 낼 수 있는 모든 순서에 대해 반복한다. 8. 핵심 DFS 구조 결국 이 문제의 핵심 코드는 다음과 같은 순열 구조 이다. static void dfs(int depth) { if (depth == 9) { int gyuScore = 0; int inScore = 0; for (int i = 0; i < 9; i++) { int sum = gyuYoung[i] + selected[i]; if (gyuYoung[i] > selected[i]) { gyuScore += sum; } else { inScore += sum; } } if (gyuScore > inScore) { win++; } else { lose++; } return; } for (int i = 0; i < 9; i++) { if (visited[i]) continue; visited[i] = true; selected[depth] = inYoung[i]; dfs(depth + 1); visited[i] = false; } } 이 코드를 순열의 관점에서 다시 보면 dfs(depth) 1. depth == 9 → 순열 하나 완성 → 경기 결과 계산 2. 아직 완성되지 않았다면 for (모든 카드 후보 i) 이미 사용했으면 continue 카드 선택 visited[i] = true 현재 자리에 카드 저장 selected[depth] = inYoung[i] 다음 자리 결정 dfs(depth + 1) 원상복구 visited[i] = false 가 된다. 9. 시간복잡도 인영이 카드 9장의 모든 순열을 확인한다 (9!) 그리고 하나의 순열이 완성될 때마다 최대 9번의 카드 비교를 수행한다. 따라서 시간복잡도는 O(9! × 9) 이다. 9! = 362,880 이므로 완전탐색으로 충분히 해결할 수 있다. 10. 이 문제에서 배운 점 처음에는 카드게임의 승패를 계산하는 것에 집중해서 순열 문제라는 것을 바로 파악하기 어려웠다. 하지만 문제에서 고정된 것과 변하는 것을 구분해보니 규영이의 카드 순서는 고정되어 있고, 인영이가 카드를 내는 순서가 계속 달라진다는 것을 확인할 수 있었다. 따라서 인영이의 9장의 카드를 순서를 고려하여 나열하는 순열 문제로 접근할 수 있었다. 또한 순열 DFS에서 사용되는 변수의 역할을 다음과 같이 정리할 수 있었다. depth → 현재 결정할 자리 i → 현재 자리에 넣을 후보 visited → 이미 사용한 후보인지 확인 selected → 현재까지 만든 순열 특히 DFS 코드를 단순히 암기하기보다 선택 → 재귀 → 원상복구 → 다른 선택 이라는 흐름으로 이해하는 것이 중요하다는 것을 배웠다. 앞으로 문제에서 “N개의 원소를 순서를 고려하여 배치하고 모든 경우를 확인해야 한다” 는 특징이 보이면 순열과 백트래킹을 먼저 생각해볼 수 있을 것 같다. 전체 구현 코드 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.Arrays; import java.util.StringTokenizer; // 규영이와 인영이 public class Solution { static int guWin, guLose; static int[] guCard; static int[] inCard; static boolean[] visited; static int[] selected; static void dfs(int depth, int guScore, int inScore){ // depth == 9 일때 점수 계산 if(depth == 9){ if(guScore>inScore){ guWin++; }else{ guLose++; } return; } // 인영 카드 순열 for(int i=0; i<9; i++){ if(visited[i]) continue; visited[i] = true; selected[depth] = inCard[i]; int sum = selected[depth] + guCard[depth]; if(selected[depth] > guCard[depth]){ dfs(depth+1, guScore, inScore+sum); }else{ dfs(depth+1, guScore+sum, inScore); } visited[i] = false; } } public static void main(String[] args) throws IOException { // 규영이가 이기는 경우와 지는 경우 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int T = Integer.parseInt(br.readLine()); for(int tc=1; tc<=T; tc++){ StringTokenizer st = new StringTokenizer(br.readLine()); guCard = new int[9]; inCard = new int[9]; boolean[] cardSelected = new boolean[19]; // 규영이 카드 입력 for(int i=0; i<9; i++){ guCard[i] = Integer.parseInt(st.nextToken()); cardSelected[guCard[i]] = true; } // 인영이 카드 입력 int idx = 0; for(int i=1; i<=18; i++){ if(cardSelected[i]) continue; inCard[idx++] = i; } guLose = 0; guWin = 0; visited = new boolean[9]; selected = new int[9]; // dfs (깊이, 규영이 점수, 인영이 점수) dfs(0, 0, 0); System.out.println("#"+tc+" "+guWin+ " "+guLose); } } }