어찌저찌 프로젝트가 끝났다. 주말에도 추가로 발표자료 작성이나 문서 작업은 진행해야 할 것 같지만 프로젝트의 코드를 더 건드릴 일은 이제 리팩토링말고는 딱히 안해도 될 것 같다.
다른 조보다 적은 인원수였는데도 다들 협업을 너무 잘해주셔서 이슈 처리도 금방 끝났고 코드 작성 시간의 효율이 상당히 좋아 집중할 환경이 마련된 것 같다.
물론 구체적인 컨벤션을 하나하나 따지면서 리뷰하지는 않았지만 환경이었지만 의문점, 누락된 점등은 꼬박꼬박 체크했고 납득해야 Approve 했으니 어찌됐든 의도대로 작동은 하는 환경이라 생각한다.
이번이 첫 프로젝트니 다음 프로젝트에서 다들 쌓이셨을 협업 경험으로 더 딥하게 나가면 된다고 생각한다.
클라이언트에서만 써보던 걸 실제로 서버로 구현해보게 됐다. 어떻게 했었는지는 정확히 기억 안나긴 하는데 카카오 로그인 SDK로 클라이언트에서 토큰까지 얻어오고 그걸 서버로 보내면 처리가 됐었던 걸로 기억한다.
그 뒷 내용을 오늘 조사해서 프로젝트에 끼워보기로 했다.
우선 클라이언트가 없으니 클라이언트 땜빵칠 resources/static 에 html 파일을 대충 넣어서 체크해보기로 했다.
다만 어디까지나 정적 페이지라서 데이터가 동적으로 바뀌거나 하는건 불가능하고 동적 페이지를 지원하는건 resources/template 에 작성후 thymeleaf 탬플릿 엔진을 사용하면 바꿀 수 있는데 아직 써보진 않기로 했다.
(탬플릿 엔진: html 태그에 속성을 추가해 페이지에 동적으로 값을 추가하거나 처리할 수 있다.)
이거는 별 작동을 안해서 의미 없으니 삭제하고 그냥 GET으로 나중에 다 바꾸게 됐다.
OAuth는 Open Authorization의 약자로 비밀번호를 쓰지 않고 다른 서비스/어플리케이션의 정보를 갖다줄 수 있게 해주는 인증 프레임워크다. 일반적으로 소셜 로그인에 많이 사용된다.
외부 서비스에서 요청만 하면 사용자 정보를 갖다줄 수 있으니 편하지만 그만큼 접근과 제공에서 보안을 신경써야 한다.
OAuth2인 이유는 OAuth 1.0이 있었기 때문인데 OAuth1은 서명 기반으로 클라이언트에서 서명을 생성해서 암호화 후 서버와 검증을 해야하는지라 매우 복잡해진다.
2에서는 주로 JWT를 사용해서 HTTPS 통신으로 보안을 유지하기 때문에 접근이 간단하고 구현 비용도 싸진다.
서명의 검증이 없어졌기 때문에 더 다양한 로직을 써먹을 수 있어 어지간해선 OAuth2가 쓰인다고 보면 될 것 같다.
카카오 로그인을 구현하는 여러 레퍼런스를 참고하고 GET /oauth/authorize에서 Redirect를 통해 받은 인가 코드를 다시 POST로 요청보낼 때 Rest template를 썼는데 이게 여러모로 자주 보이는데 뭔지 몰라서 조금 찾아봤다.
스프링에서 제공하는 HTTP 클라이언트 라이브러리라고 하는데 그냥 Retrofit, Alamofire 처럼 RestAPI 제공해주는 클라이언트인 것 같다.
하지만 RestTemplate는 Spring 3부터 제공된 오래된 클라이언트고 Spring 5부터 제공해주는 WebClient는 Non-blocking 처리가 가능하다고 해서 버전에 구애받지 않는다면 WebClient를 더 선호하기 시작했다고 한다.
이번엔 레퍼런스들이 RestTemplate를 쓰길래 그대로 써보기로 했지만 나중엔 직접 공부해서 바꿔봐야 할 것 같다.
POST /oauth/token을 처리하는 과정에서 요청을 담는 HttpEntity
생성중에 Param을 MutableMap 담아서 보냈는데 다음 에러가 발생했다.
No HttpMessageConverter for java.util.LinkedHashMap and
content type "application/x-www-form-urlencoded;charset=utf-8"
에러를 보고 Rest template에 Converter를 찾아봐야하나 싶어서 열심히 이거저거 추가해봐도 해결되지 않았고 근본적인 설정의 원인이 있겠거니 싶어 찾아보니 다른 곳에서는 LinkedMultiValueMap을 구현해서 사용하고 있었다.
둘 다 Map을 상속받긴 했는데 MutableMap은 한 키에 한 값을 가지는 쌍이고 MultiValueMap은 Spring에서 제공하는 한 키에 여러 값을 가지는 맵이다.
여기서 차이가 나오는데 내가 content-type으로 설정한 application/x-www-form-urlencoded
는 한 키가 여러개의 쌍을 가지는 걸로 처리해서 그냥 단순히 MutableMap을 변환하지 못한 것이다.
HttpEntity의 body는 T로 선언되어 있어 아무거나 넣으면 Rest template가 Converter를 찾아 적절하게 변환이 이루어지는데 이거 참 몰라서 발생한 문제다.
하다보니 유저 정보까지는 우여곡절끝에 얻어오게 됐다. 문제는 카카오가 정책때문에 비즈니스 앱이 아닌 테스트 앱은 Email 권한을 주지 않아서 닉네임, 프로필 사진밖에 못불러오는 카카오 로그인이 되었다.
이걸 가지고 로그인을 하려면 현재 유저정보 얻어오는 과정을 REST Api로 만들어서 Header로 액세스 토큰을 받고 Body로 이메일등을 추가로 받아야하고 이대로는 그냥 테스트 툴이나 다름 없긴 하다.
실제 서비스에선 이 데이터 관리가 전적으로 로직과 유저 DB로 이루어졌을텐데 참 머리가 아프다. 특히 여러 소셜 로그인으로 한번에 구현하는 경우의 처리도 참 생각할 점이 많아질 것 같다.
이번엔 단순 OAuth 진행으로 남기기 좀 그래서 억지로 Controller의 Param을 늘리고 user service에 내용을 추가해서 추악한 레거시 코드 그 자체로라도 구현을 급하게 진행했다. User에 Provider와 Provider id를 남겨 관리를 해야할 것 같긴 한데 우선은 Provider만 추가해서 OAuth로 회원가입 / 로그인 했다는 증거만 남겼다.
어차피 지금처럼 이메일 권한을 못얻으면 클라이언트에서도 유저가 이메일을 바꿔서 보내면 똑같이 되니까 임시로 되는지만 체크해보는 걸로 만족하기로 했다.
햄버거 가게에서 일을 하는 상수는 햄버거를 포장하는 일을 합니다. 함께 일을 하는 다른 직원들이 햄버거에 들어갈 재료를 조리해 주면 조리된 순서대로 상수의 앞에 아래서부터 위로 쌓이게 되고, 상수는 순서에 맞게 쌓여서 완성된 햄버거를 따로 옮겨 포장을 하게 됩니다. 상수가 일하는 가게는 정해진 순서(아래서부터, 빵 – 야채 – 고기 - 빵)로 쌓인 햄버거만 포장을 합니다. 상수는 손이 굉장히 빠르기 때문에 상수가 포장하는 동안 속 재료가 추가적으로 들어오는 일은 없으며, 재료의 높이는 무시하여 재료가 높이 쌓여서 일이 힘들어지는 경우는 없습니다.
예를 들어, 상수의 앞에 쌓이는 재료의 순서가 [야채, 빵, 빵, 야채, 고기, 빵, 야채, 고기, 빵]일 때, 상수는 여섯 번째 재료가 쌓였을 때, 세 번째 재료부터 여섯 번째 재료를 이용하여 햄버거를 포장하고, 아홉 번째 재료가 쌓였을 때, 두 번째 재료와 일곱 번째 재료부터 아홉 번째 재료를 이용하여 햄버거를 포장합니다. 즉, 2개의 햄버거를 포장하게 됩니다.
상수에게 전해지는 재료의 정보를 나타내는 정수 배열 ingredient
가 주어졌을 때, 상수가 포장하는 햄버거의 개수를 return 하도록 solution 함수를 완성하시오.
class Solution {
fun solution(ingredient: IntArray): Int {
var answer: Int = 0
val stringBuilder = StringBuilder()
ingredient.forEach {
stringBuilder.append(it.toString())
if (stringBuilder.length > 3 &&
stringBuilder.substring(stringBuilder.length - 4) == "1231") {
stringBuilder.setLength(stringBuilder.length - 4)
answer++
}
}
return answer
}
}
처음엔 순서대로 Stack을 쌓아서 접근하다가 1,2,3,1이 나오면 처리해야겠다 라고 생각이 들었다. 그런데 1이 나왔을 때 앞이 1, 2, 3인걸 확인하는 구조를 처리하기엔 Stack이 적절한가를 판단하느라 고민이 많아졌다.
이리저리 알아보다가 LinkedList는 next, previous를 알고 있으니까 괜찮지 않나 하고 찾아보니 옛날에 C에서 구현하던 모습과는 다른건지 내가 까먹은건지 도통 못써먹겠어서 포기하고 String을 쌓아서 풀기 시작했다.
그런데 String으로 풀다가 무심코 String의 대입을 쓰기 시작한걸 느끼고 부랴부랴 StringBuilder로 바꾼게 지금의 모습이 됐다. 무식하지만 생각보다 속도가 나쁘지 않길래 신기했다.
제출하고나서 Stack으로 푼 답안이 있는지 찾아봤는데 당연히 있었다.
fun solution2(ingredient: IntArray): Int {
var answer: Int = 0
var stack = Stack<Int>()
ingredient.map { value ->
stack.add(value)
if (stack.size >= 4 && stack.peek() == 1) {
val first = stack[stack.size - 4] == 1
val second = stack[stack.size - 3] == 2
val third = stack[stack.size - 2] == 3
val fourth = stack[stack.size - 1] == 1
if (first && second && third && fourth) {
repeat(4) { stack.pop() }
answer ++
}
}
}
return answer
}
그런데 상상과는 좀 여러모로 다른 모습이긴 했다. 내가 Stack을 쓰려고 했던 add, pop을 하는 이유는 동일하지만 해당 위치의 원소를 찾는건 딱히 뾰족한 수가 없긴 한가보다.