주말까지 해서 강의가 제공하는 수강신청 Application의 모든 Layer 작성은 마쳤다. 이제 추가로 공부할 부분은 테스트, JWT고 이 정도 개념까지 잡히면 ERD Cloud와 API 설계를 고민하고 마친 다음 실제 작성으로 들어가게 될 것 같다.
사실 빨리 해보고 싶은게 수강신청 API 설계는 강의를 길게 듣다 보면 계속 까먹어서 안보고 짜기가 꽤 험난해서 결국 스니펫을 그대로 참고하는 경우가 많았는데 이번엔 확실히 내 기반으로 짜게 될테니 어디가 틀렸고 나은 구조가 있는지를 고민할 수 있을 것 같다.
테스트 코드
기본적으로 Swagger 에서 쉽게 문서를 확인하고 테스트 할 수는 있지만 매번 테스트하기 좀 어려울 수 있다.
- 테스트는 크게 4가지는 봐야한다.
- 정상 상황일 때 Response Body가 적절한지
- 정상 상황일 때 Status Code가 적절한지
- 비정상 상황일 때 Response Body가 적절한지
- 비정상 상황일 때 Status Code가 적절한지
- 테스트 순서도 잘 고려해야 한다.
- 강의 App의 경우 Course가 있어야 Lecture, CourseApplication이 가능하고 CourseApplication은 User가 있어야 한다 같은 정책으로 잘 구분해야 한다.
- 이런 테스트 케이스는 무난하게 엑셀같은 도구가 딱 적당할 수 있다.
- application.yml 에서 jpa properties를 설정해서 Application 로그에서 SQL이 정상적으로 나가는지 테스트 해볼 수 있다.
- jdbc logging 을 설정해서 실제 들어가는 값도 확인할 수 있다.
양방향, 단방향
- 강의는 DB를 기준으로 양방향으로만 진행했지만 객체에선 단방향을 설정함이 좋다.
- 튜터님은 양방향 관계를 지양하는게 좋다고 하셨다.
- 양방향의 경우 양쪽 객체 모두 무결성을 보장하는지 체크해야해서 어려워진다.
- 객체지향적으로 한 쪽만 체크하고 관리할 수 있다.
- 양방향에서 주인이 아닌 Entity는 Cascade를 설정하지 않는 게 바람직하다. 조회의 편의성을 위한 것일 뿐이다.
- 단방향에서 설정한 save, delete는 기대한 대로 잘 전파된다.
- 단방향이면 연관관계의 주인을 제외하고 mappedBy를 빼주고 외래키가 어떤 Column인지 JoinColumn을 표기해준다.
- 단방향으로 바꿨을 때 OneToMany라면 Lecture 추가에서 Course id를 제거하면 우선 course_id 없이 Insert를 진행하고 Update로 course_id를 설정해 연관을 맺어준다.
- FK가 NOT NULL이라면 Insert에서 에러나기 때문에 안된다.
- FK에 NOT NULL 제약을 걸 수 없어서 피하는 것이 좋다.
- ManyToOne이라면 외래키 관리 주체가 DB와 같아지므로 파악이 쉽다. (course_id를 계속 들고 있던 건 lecture였다.)
- 대신 가능했던 Course -> Lecture 역방향 접근이 불가능해지고 Lecture를 확인하려면 추가적인 쿼리가 필요해진다.
- 양방향은 개발 편의성이 좋지만 주체가 불명확하고 Entity의 상태가 불일치할 가능성이 존재해서 개발자가 Entity 상태와 쿼리를 확실히 파악하기 힘들다.
- 양방향을 짜더라도 연관관계의 주체를 확실하게 잡고 설계를 해야 불일치같은 상황을 방지할 수 있다.
- 지난주-주말까지 공부했던 내용중 '이게 명시적이고 효율적인가?' 고민했던 부분들은 전부 양방향에서 발생한 문제였고 단방향으로 표기하면 고민을 싹 지울 수 있다.
쿠키와 세션
솔직히 백엔드하면 이 쪽부터 떠올렸었는데 지금까지 해온 기반은 프론트였던 내가 쓰면서 익숙하던 부분들이었다면 이제는 진짜 한 번 써보고 말았던 부분들이라 참 개념이 무섭다.
- 여태까지 해온 Bean으로 하는 관리는 Request, Response 까지만 관리하고 사라지는 stateless 상태였는데 데이터를 유지하기 위해 쿠키 / 세션에 상태를 저장한다.
- 쿠키는 클라이언트(브라우저)에 저장하고 서버에서 세팅해서 클라이언트에 저장한 후 서버에 다시 요청을 보낼 때 그 정보가 담긴다.
- 이름, 값, 유효시간, 도메인, 경로가 포함된다.
- 세션은 서버측에서 클라이언트의 상태를 유지하는 기술로 고유한 Session ID를 부여해서 클라이언트의 정보를 보관하고 관리한다. 이 세션 ID를 클라이언트에 부여하는 부분에선 쿠키를 사용한다.
- ID 외 클라이언트 정보를 서버측에서 관리해야하기 때문에 별도 메모리/저장소를 이용한다.
- 서버를 여러 개 운용하게 될 경우 확장성이 좋지 않다.
- 별도의 저장소를 운용해서 세션 ID를 관리해도 그 저장소와 추가로 확인이 필요하기 때문에 오버헤드가 발생한다.
토큰 인증
써봤으니 이 쪽이 더 익숙하고 관리가 편한 것도 맞다. 물론 개념 자체가 어려웠고 클라이언트에서 토큰을 관리하는 과정도 쉽지만은 않았다.
- 인증 정보를 토큰으로 만들어 클라이언트가 토큰을 소유하게 만드는 방식
- 브라우저에도 토큰 저장이 되는 로컬 스토리지가 있고 서버에 요청을 보낼 때 토큰을 첨부해서 보냄
- 서버는 이 토큰이 정상적인 토큰인지 검증을 해서 응답함
- 검증 방식을 똑같이 사용한다면 같은 플랫폼의 다른 서비스의 로그인 시스템에서도 인증 정보를 그대로 사용할 수 있음
- 서버에 뭔가 저장하지 않는다는 점 때문에 Stateless 하다고 볼 수 있음
- 클라이언트에 저장하기 때문에 보안 이슈로 토큰에 민감 정보를 담으면 안되고 세션 ID에 비하면 토큰의 사이즈는 훨씬 클 수 있어 통신에 부하가 추가로 발생함
JWT
- 토큰을 헤더-내용-서명 (Header-Payload-Signature) 구조로 이루어진 문자열로 저장함
- Header-Payload는 JSON 데이터인데 base64로 인코딩한 값을 담았음
- Header
- alg(서명 알고리즘), typ(토큰 타입) 을 담고 있고 typ는 보통 JWT로 고정하고 쓰는데 확장성을 고려해서 구분하기 위해 존재함
- Payload
- 서버에서 설정한 사용자 권한 정보와 데이터가 담겨있고 Key, value로 저장된 정보를 Claim 이라 표현함.
- Registered Claims라고 포함하길 권장하는 claim 목록이 있음
- iss(Issuer): JWT 발급 주체(발급자)를 표기함
- sub(Subject): JWT를 받는 대상(유저)를 표기함
- aud(Audience): JWT를 받을 그룹을 표기함
- 여러 시스템에서 써야할 수도 있고 한 시스템에서 권한이 나뉠 수 있으니 그런 그룹을 구분함
- exp(Expiration time): JWT 만료 시간, Timestamp 형태로 사용
- nbf(Not before time): 해당 시간 전에 JWT가 사용될 수 없음. 예약 작업처럼 특정 시간 전에 작업하면 안되는 걸 표기할 수 있을듯? 자주 사용안함
- iat(Issued at time): JWT가 발급된 시간
- jti(JWT ID): JWT의 고유 ID로 중복된 JWT 생성을 방지하고 Replay Attack을 방지하기 위해 씀
- 저런 거 말고 필요한 거를 담으면 Custom claims라고 함
- 유저의 민감정보가 아닌 email, roles 같은 걸로 권한을 구분하는 용도로 쓰이기도 함
- Signature
- 서버가 검증을 하기 위해 가장 중요한 값
- Header, Payload를 합쳐 암호화한 후 Base64로 한 번 더 인코딩 한 값
- 암호화는 대칭키, 비대칭키를 이용한 알고리즘으로 나뉨
- 대칭키는 키 하나를 가지고 암호화, 복호화를 진행함
- 비대칭키는 개인키, 공개키로 진행하고 공개키로 암호화하면 개인키로 복호화, 개인키로 암호화하면 공개키로 복호화하는 방식으로 주고받음
- 대칭키는 키 관리가 중요하므로 단일 시스템에서 통신할 때 적합
- 비대칭키는 공개 키는 공유해도 되므로 여러 시스템이 서로 토큰을 공유하고 검증할 때 적합
- 대칭 키 알고리즘은 주로 HS256, 비대칭키는 RS256인데 256은 SHA256 해쉬 알고리즘을 의미함.
- 어떤 문자열이든 동일한 길이로 만들고 만들기만 하고 풀 수 없는 비가역적 알고리즘
- 만료 시간을 관리하기 위해 Access token, Refresh token 두 가지로 구분해서 Access token이 만료되면 Refresh token으로 재발급을 요청한다.
- Refresh token의 만료를 방지하기 위해서 Access token 재발급 때 Refresh token도 재발급을 하는데 이렇게 하면 이용중일시 계속 Refresh token 기간이 늘어나고 이러한 방법을 Sliding Session 이라고 한다.
과제 기초 설계하기
강의에서 설계가 탄탄해야 하는 이유를 많이 알 수 있을 정도로 강의의 볼륨이 심상치 않았는데 이걸 다 보고 나는 과제인 Todo 서버의 설계를 다 짜는 입장이 됐다.
회사 다닐 때도 PM분과 함께 이슈 태스크를 작성하거나 프로젝트 설계 회의에 잘 모르지만 들어가거나 했었지만 아예 기반부터 백엔드 설계로 들어가는 건 꽤나 생소했다.
Use case diagram
일단 Use case diagram 부터 작성했는데 구글 검색 결과에서 나오는 이미지를 보고 '원래 이렇게 좀 유치한가?' 생각이 먼저 들었다.
일단 Actor 역할을 하는 유저 이미지부터가 졸라맨을 써서 그런지 상당히 저렴한 이미지였는데 그런 것 치고는 직접 그리려니 내용을 어떻게 구분하고 연결할지 감이 잘 안와서 여러번 지우고 그리다가 결국 가장 심플한 형태를 선택하게 됐다.
그리고 하다보니 정보처리기사에서 최근 공부한 내용도 떠올렸는데 이런 걸 개념적으로 풀어서 문제로 내면 내가 공부한 그 내용들이 되는 건가 싶기도 했다.
Use case diagram
API 명세서
이거는 조금 더 다양한 형태가 있고 주로 엑셀같은 느낌으로 작성하는 거라 그냥 과제 페이지에 있는 API 명세서 견본을 그대로 가져와서 썼다.
일단 URI는 써봤던 것처럼 /api/v1/ 형태로 가져가기로 했고 패키지 구조도 가능하면 그렇게 짜려고 한다.
다만 명세서를 채우는 내용은 어떤 양식을 따라야하나 고민하다가 정책도 좀 채우긴 했는데 뭔가 부실해보임을 느꼈다. 내가 앱 개발 할때 이거 띡 줬으면 무슨 생각을 할까를 고민하며 짰는데 좀 아쉬운 것 같다.
물론 이건 명세서고 실제 Docs는 Swagger 쓰거나 더 세부적으로 작성하겠지만 그냥 그런 아쉬움이 들었다.
API 명세서
ERD Cloud
동기분이 Mermaid.js를 알려주셔서 Readme에 편하게 넣기 위해 그걸 써보려고 했는데 이건 구조를 텍스트로 작성해야 해서 지금 내 깜냥에 그러긴 힘들 것 같아 그냥 ERD Cloud로 작성했다.
ERD Cloud를 Mermaid 로 export 하는 기능이 있으면 참 좋을 것 같은데... 하는 생각은 든다.
이번엔 ERD 작성부터 내가 생각하는 Soft delete, 상태로 레코드 관리, 로깅을 위한 Timestamp 관리등 여러가지 욕심을 선투입 시켜놔서 이걸로 먼저 작성하면 Step 1 Entity 작성도 더 오래 걸리지 않을까 싶지만 일단 해보기로 했다.
ERD Cloud
챌린지반 - 객체지향(1)
첫 챌린지반 수업이 진행됐다. 객체지향적 프로그래밍에 대해 설명을 잘해주시는 튜터님이셔서 이번 커리큘럼도 객체지향부터 배우게 됐다.
- 객체지향에서 지향하는 부분은 각 객체가 자기 역할에 책임지고 충실해야하고(전문성) 내가 처리 못하는 일은 다른 객체를 믿고 그 일을 요청하는 것
- 손님은 홀 직원이 주문을 받고 나서 무슨 일 하는지 알 필요가 없다.
- 홀 직원한테 주문을 전달받은 셰프는 셰프가 다른 사람으로 바뀌더라도 요리 할 줄 아는 셰프면 책임을 다 하는 것
- 객체지향에서 의존성은 변경을 전파하기 때문에 최소화해야한다.
- 객체지향의 4대 특징(캡상추다)중 핵심은 클라이언트가 의존하지 못하도록 은닉하는 캡슐화가 핵심이라고 생각한다.
- 클라이언트 객체의 시점에서 의존할 수 있는 내용을 항상 고려해야 한다. 가급적 추상적인 것을 의존하게 해야한다.
- 캡슐화를 위해 Client 객체를 먼저 완성하고 어떤 부분을 요청하는지 (추상적인 내용) 결정될 때까지 구체적인 내용을 작성하지 않는 것도 캡슐화를 위한 노력이라 볼 수 있다.
- 예) OAuth 로그인도 OAuth2Client interface를 만들고 오로지 서비스에선 login() 만 알게 하면 인터페이스를 상속받은 각 OAuth2 클라이언트를 만들어 login만 알게 하는 캡슐화가 가능해지고 추상화와 다형성또한 자연스레 따라오게 된다.
- 이 예에서 클라이언트 객체인 OAuthLoginService를 만들고 있었다면 login() 만 적고 구체적인 것은 전부 넘겨서 코드가 깔끔하고 의존성을 최소화할 수 있다.
- KakaoOauth2Client를 의존하고 있었을 땐 OAuthLoginService가 높은 객체, Kakao~가 낮은 객체였지만 OAuth2Client interface를 만들고 의존하게 되면 OAuth2Client 가 높은 객체가 되고 Client가 낮은 객체가 되는 식으로 상대적인 연관 관계를 의존성 역전으로 보는 것이다.
최근 구조적인 부분을 새로 배울 때 앱에서 느꼈던 불편함 방지를 위해 캡슐화, 추상화를 꽤 고려하고 그 이유를 찾고 있는데 그런 이유를 다시 한 번 정리하는 시간이 됐다고 생각한다.
개발 편의성만 고려하면 나중에 이슈처리, 유지보수의 복잡함이 한층 올라가는 것을 명심하자. 일단 앱 개발 당시의 나는 객체 사이즈부터 합리적인 방법으로 줄여야 한다.
코드카타 - 프로그래머스 모의고사
수포자는 수학을 포기한 사람의 준말입니다. 수포자 삼인방은 모의고사에 수학 문제를 전부 찍으려 합니다. 수포자는 1번 문제부터 마지막 문제까지 다음과 같이 찍습니다.
1번 수포자가 찍는 방식: 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
2번 수포자가 찍는 방식: 2, 1, 2, 3, 2, 4, 2, 5, 2, 1, 2, 3, 2, 4, 2, 5, ...
3번 수포자가 찍는 방식: 3, 3, 1, 1, 2, 2, 4, 4, 5, 5, 3, 3, 1, 1, 2, 2, 4, 4, 5, 5, ...
1번 문제부터 마지막 문제까지의 정답이 순서대로 들은 배열 answers가 주어졌을 때, 가장 많은 문제를 맞힌 사람이 누구인지 배열에 담아 return 하도록 solution 함수를 작성해주세요.
제한 조건
- 시험은 최대 10,000 문제로 구성되어있습니다.
- 문제의 정답은 1, 2, 3, 4, 5중 하나입니다.
- 가장 높은 점수를 받은 사람이 여럿일 경우, return하는 값을 오름차순 정렬해주세요.
문제 링크
fun solution(answers: IntArray): IntArray {
val onePattern = intArrayOf(1, 2, 3, 4, 5)
val twoPattern = intArrayOf(2, 1, 2, 3, 2, 4, 2, 5)
val threePattern = intArrayOf(3, 3, 1, 1, 2, 2, 4, 4, 5, 5)
var (one, two, three) = Array(3) { 0 }
answers.forEachIndexed { index, answer ->
if (onePattern[index % onePattern.size] == answer) { one++ }
if (twoPattern[index % twoPattern.size] == answer) { two++ }
if (threePattern[index % threePattern.size] == answer) { three++ }
}
val answerArray: IntArray = intArrayOf(one, two, three)
.mapIndexed { index, value -> index + 1 to value }
.filter { it.second == intArrayOf(one, two, three).maxOrNull() ?: 0 }
.map { it.first }
.toIntArray()
return answerArray
}
이번 문제는 금방 풀었고 시간 복잡도도 O(n)으로 어려울 거 없게 풀었다.
제출 전에 고민한 부분은 저 지저분한 one, two, three 처리를 어떻게 할 것인가였고 계속 짱구를 굴려봤지만 더 Kotlin 스럽게 표현한다던가 조건문을 줄인다던가 하는 방안이 안떠올라서 그냥 다른 부분 코드를 간결화했다.
아무리 생각해도 패턴을 100개가 있다고 가정한다면? 이라고 가정해봐도 제어문을 더 수정할 방법이 떠오르지 않았다.
일단 one, two, three를 var로 하나하나 선언했던걸 한줄로 묶어서 Destructuring으로 선언해봤고 답변을 저장하는 부분도 그냥 max로 if문 처리해서 array에 더하던 거를 kotlin 스럽게 작성했다.
그런데 솔직히 이건 있어보이기만 하고 효율은 더 별로긴 하다. 물론 pattern을 100개씩 주면 이 코드가 보기 깔끔하겠지만 그러면 애초에 위에서 하드코딩을 때려박았을테니 그냥 보기만 좋다고 여기기로 했다.
제출하고 다른 풀이를 참고해봐도 하드코딩이 대부분이었고 패턴 100개가 들어와도 처리할 수 있게 패턴과 정답 목록의 이중 반복으로 작성하신 분도 계시긴 하셨다.
이번 문제는 고려할만한 포인트는 그 정도인 것 같다.