240528 게임 뉴스피드 프로젝트 - User와 Jwt 토큰 사용해보기

노재원·2024년 5월 28일
0

내일배움캠프

목록 보기
48/90

무수히 많은 PR 리뷰하며 User 도메인 완성하기

기존에 진행한 Todo랑 크게 다를 바 없을 줄 알았는데 생각보다 이런저런 수정을 많이 거쳤다.
혼자서는 잘 발견 못하던 오타, 일관적이지 못한 메소드, 필드 이름같은 게 리뷰에서 속속들이 걸렸고 ERD 명세와 다른 점들도 발견돼서 자꾸 커밋해서 리뷰를 resolve 해나갔는데 이 과정 자체가 참 좋은 것 같았다.

나도 다른 분들의 PR을 열심히 리뷰했고 미약하지만 컨벤션을 정하고 의견을 공유하는 시간을 가지니 프로젝트가 하루만에 쑥쑥 발전해나가는 점이 눈에 띄었다. 다른 분들의 PR을 리뷰하다보면 기분이 나쁘지 않을지, 내가 엄격한 건지 괜히 생각도 들곤 했는데 다들 적극적으로 의견을 나눠주셔서 짧은 우려가 됐다.

기본 비즈니스 로직까지는 어떻게든 완성했고 핵심이 되는 권한 내용이 빠져있어 나는 곧바로 Jwt 토큰 생성을 도전하기로 했다.

Jwt 토큰 만들기

대체로 레퍼런스를 살펴보면 대부분이 Spring security를 사용한다. Spring security를 통해서 어플리케이션 전반적인 보안을 검증하기 좋아 사용하는 걸로 추정되는데 나는 이번 Todo 과제에서 튜터님이 보여준 Spring security 없이 jjwt 라이브러리만 사용해서 Jwt를 만들고 Authorization header에서 받아오는 것 정도만 우선 구현하기로 했다.

우선 Secrets는 항상 숨겨야지 생각했기 때문에 JwtProperties를 만들고 @Configuration으로 등록하고 @ConfigurationProperties을 사용해서 prefix가 jwt인 application.yml의 secrets를 읽어오기로 했다.

yml을 읽어오는 과정이 꽤 어려울 줄 알았는데 생각보다는 쉬웠고 이걸 이용해서 userId를 저장해 생성, 검증 딱 두개만 진행 가능한 JwtPlugin을 만들었고 거의 따라치긴 했지만 Claim 생성 과정과 expiration 설정, 그리고 Key 생성은 어느정도 이해했다. (hmacShaKeyFor을 사용했다.)

이를 @Component Bean으로 등록해서 User service에 주입해서 SignIn까지 구현하니 나름 로그인시 토큰 발급까지 잘 되는걸 확인했다.

이제 앞으로 이 토큰가지고 고생해야하는 건 어쩔 수 없겠지만 userId를 계속 body에 집어넣고 테스트할 수 없는 노릇이니 이것도 꽤 만족스럽다고 생각한다.

물론 RefreshToken까지 채용한 짧은 AccessToken도 아니고 Spring container를 통한 주입방식 말고 다른 방법은 없는지도 의문이고 되게 간단한 플러그인임은 부정할 수 없겠다.

튜터링 - Repository에 대한 이해

오늘 튜터님 이전 Todo 과제에서 끊임없는 의문을 생산해냈던 Aggregate 경계와 Todo가 User를 조회하는 방법에 대해 많은 의문이 해결됐다.

JpaRepository는 Interface인 것부터 시작해서 JpaRepository는 저수준 모듈일지 고수준 모듈일지에 대한 이해부터 시작했는데 Todo service가 User service를 찾아오는 건 결국 User JpaRepository를 통해 얻어오는 데이터가 된다.

그리고 구체적인 User에 대한 정보를 얻어올 수도 없다고 판단할 수 있다. Service는 Entity를 반환하지 않게 기준을 확실히 하기로 했기 때문이다.

UserRepository와 UserJpaRepository 인터페이스를 구분하고 아무것도 상속받지 않은 UserRepository, UserRepositoryImpl을 의존하게 된다면 자주 변경될UserJpaRepository의 변경에 영향을 받지 않을 수 있고 의존성 역전이 성립된다.

내가 만들 Service들은 구체적인 JpaRepository가 아니라 기본 Repository 저장소를 구현해 의존했다면 Jpa의 영향을 최소화 할 수 있고 항상 고고히 존재했던 Repository의 책임 또한 분리할 수 있다.

이는 한 Service를 Application service와 Domain service로 구분한다는 예시를 봤었던 것과 유사하다고 볼 수 있다.

DDD의 원칙을 지켜보겠다고 공부하고 있었지만 오늘 SOLID 원칙부터 시작해서 도메인 기반의 설계에 대한 내용을 확실하게 볼 수 있어 참 재밌는 시간이었다.

여기서 튜터님은 완전히 멀티모듈로 Spring의 종속성까지 없애는 예시를 보여주셨는데 Spring container가 필요한 부분도 분리되는 장면은 가히 충격적이었다고 할 수 있다. 언젠가 토이 프로젝트를 하게 된다면 Domain 딱 2개를 두고 직접 구현은 해보고 싶다는 생각이 들었다.

나는 일단 UserService의 주입을 갖다 치우고 UserRepository, UserRepositoryImpl, UserJpaRepository를 구분해서 사용하는 것부터 생각해보기로 했다.

도메인 모델

그리고 튜터님과의 대화에서 도메인 모델에 대한 내 이해도 많이 부족했는데 대체로 model 폴더를 떠올려서 Entity와 대조하며 생각했던 거랑 달리 특정 비즈니스 도메인의 내용물을 전부 종합하면 도메인 모델이라고 할 수 있겠다.

한 도메인 모델을 구성하는 요소에 엔티티, VO, Aggregate, Repository, Service같은 게 존재한다고 생각할 수 있다.

Order 도메인 모델을 구성한다면 User, Order, MoneyVO, OrderRepository, OrderService를 종합해 비즈니스 도메인의 개념을 종합적으로 반영한 객체 지향 설계 패턴이라고 하는데 구체적으로 뭐에요? 라는 질문엔 아직 답변할 정도로는 모르겠다.

찾아보기론 MSA에서 도메인 모델에 맞춰서 구분하는 경우가 있었는데 DDD로 잘 나누어진 경우로 튜터님이 말씀하신 바운디드 컨텍스트(Bounded Context) 에 대한 이야기도 나왔다.

바운디드 컨텍스트는 한 도메인 모델이 유효한 경계를 말하고 이건 내가 말한 Aggregate 경계보다 상위 개념으로 느껴진다. 복잡한 시스템을 작은 부분으로 쪼개는 클린 아키텍쳐적인 느낌도 있다.

Aggregate의 경계는 바운디드 컨텍스트 내에서 도메인 객체의 그룹을 나누는 경계일 뿐이다.

예로 본 서비스는 전자 상거래의 경우고 내부에 여러 Aggregate을 구분할 수 있다.
1. 사용자 도메인: User, Role, Permission
2. 주문 도메인: Order, OrderItem, Payment
3. 상품 도메인: Product, Category, Inventory
4. 결제 도메인: Shipment, Address, DeliveryStatus

으로 도메인 모델을 구분할 수 있다고 가정했을 때 자기 서비스에 맞는 구조를 짜는 건 쉽겠지만 다른 도메인의 정보를 필요로 한다면 통신이 필요해진다.

독립적으로 배포되기 때문에 당연히 다른 도메인 모델간의 JPA 의존은 아예 사라지고 이에 따른 통신을 고려해야 한다.

이 구체적인 방식에 대해서는 아직 조사도 안해봤지만 자꾸 Aggregate 무새 타령하지 말고 좀 더 구체적인 범위를 머릿속에 떠올려봐야 할 것 같다.


코드카타 - 프로그래머스 대충 만든 자판

휴대폰의 자판은 컴퓨터 키보드 자판과는 다르게 하나의 키에 여러 개의 문자가 할당될 수 있습니다. 키 하나에 여러 문자가 할당된 경우, 동일한 키를 연속해서 빠르게 누르면 할당된 순서대로 문자가 바뀝니다.

예를 들어, 1번 키에 "A", "B", "C" 순서대로 문자가 할당되어 있다면 1번 키를 한 번 누르면 "A", 두 번 누르면 "B", 세 번 누르면 "C"가 되는 식입니다.

같은 규칙을 적용해 아무렇게나 만든 휴대폰 자판이 있습니다. 이 휴대폰 자판은 키의 개수가 1개부터 최대 100개까지 있을 수 있으며, 특정 키를 눌렀을 때 입력되는 문자들도 무작위로 배열되어 있습니다. 또, 같은 문자가 자판 전체에 여러 번 할당된 경우도 있고, 키 하나에 같은 문자가 여러 번 할당된 경우도 있습니다. 심지어 아예 할당되지 않은 경우도 있습니다. 따라서 몇몇 문자열은 작성할 수 없을 수도 있습니다.

이 휴대폰 자판을 이용해 특정 문자열을 작성할 때, 키를 최소 몇 번 눌러야 그 문자열을 작성할 수 있는지 알아보고자 합니다.

1번 키부터 차례대로 할당된 문자들이 순서대로 담긴 문자열배열 keymap과 입력하려는 문자열들이 담긴 문자열 배열 targets가 주어질 때, 각 문자열을 작성하기 위해 키를 최소 몇 번씩 눌러야 하는지 순서대로 배열에 담아 return 하는 solution 함수를 완성해 주세요.

단, 목표 문자열을 작성할 수 없을 때는 -1을 저장합니다.

문제 링크

fun solution(keymap: Array<String>, targets: Array<String>): IntArray {
    var answer: IntArray = intArrayOf()
    val keyCountMap = mutableMapOf<Char, Int>()
    
    keymap.forEach { key ->
        key.forEachIndexed { index, char ->
            keyCountMap[char] = minOf(index + 1, keyCountMap[char] ?: Int.MAX_VALUE)
        }
    }
    
    targets.forEach { target ->
        var keyCount = 0
        run charCountCheck@ {
            target.forEach { char ->
                keyCountMap[char]?.let { keyCount += it }
                    ?: run {
                        keyCount = -1
                        return@charCountCheck
                    }
            }
        }
        answer += keyCount
    }
    
    return answer
}

처음 떠오른 방식은 joinToString 으로 해서 keymap의 배열 Index를 구체적으로 기억하고 한 번의 순회로만 작성하기 였는데 이건 O(n^2)를 피하려고 억지로 늘려 쓰는 느낌이라 기각됐다.

그 다음은 indexOf로 대충 끼워맞추는 가장 기초적인 해설이었는데 이거는 코드를 작성하다보니 반복문 중첩이 심각할 정도로 많아져서 기각됐다. 아마 끝까지 써서 제출했으면 시간초과가 났거나 한 번에 100ms 넘게 찍힐 너무 비효율적인 방식이었다.

그래서 또 결론에 도달한 방법은 map이었는데 최근에 푼 문제는 싹 다 map으로 저장해보려다가 오히려 List로 돌아간 경우가 많아 괜히 또 문제가 생기는 거 아닌가 우려했는데 이번엔 진짜 맞는 방식이었던 것 같다.

이중 반복문이 두번씩 발생하긴 하지만 indexOf 또는 읽기 어려운 고정 index를 따질 필요 없이 keyCountMap이 어떤 글자를 누르는 최소 입력 횟수를 기억하고 있기 때문에 반례 없이 확실하게 제출을 성공할 수 있었다.

이번에 쓴 코드에서 가장 특이한 발견은 run scope에 이름을 붙여서 break의 효과를 누리는 방법이었는데 이거는 클린 코드와의 거리는 굉장히 멀어보이는 방식이지만

keyCountMap 조회의 엘비스 오퍼레이터에서 순회를 바로 중단하려고 하다보니 잘 안먹혀들고 오히려 if block이 너무 많이 늘어나거나 flag 변수를 세워야하나 고민하던 찰나에 써먹기 가장 좋은 탈출문이었다.

물론 알고리즘 문제니까 이렇게 한 번 써본 거고 실무에서는 절대 이런 식의 탈출을 고려하지는 말아야 할 것이다. 남용하면 코드의 복잡도가 상상 이상으로 올라갈 것 같다.

fun solution2(keymap: Array<String>, targets: Array<String>): IntArray =
        targets.map { str ->
            str.map { c -> keymap.map { it.indexOf(c) + 1 }
                .filterNot { it < 1 }
                .let { list ->
                    if (list.isEmpty()) -1
                    else list.minOf { it }
                }
            }.let { if ( it.contains(-1)) -1 else it.sum() }
        }.toIntArray()

여담으로 가장 Kotlin 스럽게 푼 풀이는 이 방법이었다. indexOf를 쓸 때 어쨌든 kotlin 스러운 코드는 아니었는데 진짜 작성해볼 일은 없겠지만 이건 복잡하긴 해도 메소드 체이닝으로 표현을 잘 한 것 같다.

0개의 댓글