240524 Spring 숙련 - 디테일 챙기고 Todo 과제 마무리

노재원·2024년 5월 24일
0

내일배움캠프

목록 보기
46/90

패스워드 암호화하기

한참을 미뤄오던 패스워드 암호화도 진행하기로 했다. 사실 대충 해쉬 함수가 내장되어 있다던지 하는 식으로 별 거 없을줄 알았는데 Spring security를 이용하는 걸 권장하길래 결국 Spring security를 추가하게 됐다.

Spring security의 config class를 만들어 @Configuration 정의 후 PasswordEncoder를 반환하는 Bean을 만들고 내부에선 구현체인 BCryptPasswordEncoder을 반환한다.

이걸 서비스에서 private val passwordEncoder: PasswordEncoder 처럼 주입받아 사용하면 된다.

BCryptPasswordEncoder는 BCrypt 해싱 함수를 사용한다는데 salt도 자기가 알아서 추가하고 해싱을 여러차례 걸쳐서 진행한다고 한다. 정확히는 모르겠지만 대충 꽤 안전하겠구나라는 생각은 든다.

Encoder를 사용하는 방식, 사용하는 위치는 또 제각각인 것 같은데 나는 비즈니스 로직에서 일단은 처리하기로 했다. 밑에서 다룰 주제처럼 이것도 언젠가 체크만 전문적으로 책임지는 객체를 생성하지 않을까 싶다.

여러가지 방법의 권한 체크하기

CRUD중 CUD는 거의 유저 정보를 조회해서 권한이 있는지 체크를 해야하기 때문에 비즈니스 로직과 Entity에서 유저를 조회하고 권한을 체크하는 식으로 권한을 체크하게 됐다.

그런데 hasPermission(userDto) 같은 걸 만들긴 했지만 유저 조회가 너무 많아지고 Entity에서 권한 체크하는 조건도 추가될 수도 있고 하니 뭔가 깔끔해보이지가 않았다. 그리고 매개변수로 추가로 현재 세션의 userId를 받아야 하는 것도 서비스가 커질수록 별로일 것 같다는 생각은 들었다.

그런데 이것 말고 별 방법 없지 않나? 싶어 체크해보니 방법이 더 있는 것 같다.

AOP를 이용한 권한 체크

반복되는 사용자 권한 확인을 AOP로 리팩토링하기

지금은 세션을 쓰지만 토큰으로 검증을 바꾸게 되면 토큰의 유효성 검증도 매번 진행하게 된다. 그런 점에서 AOP(Aspect oriented Programming), 관점 지향 프로그래밍 의 개념을 채택해서 코드의 중복을 해결하는 점이다.
(ex: 권한을 체크해야하는 관점 -> 세션을 이용해야하는 관점이 생기고 이 부분을 어노테이션으로 만들어 사용가능하게 한다.)

@SpringBootApplication의 @EnableAutoConfiguration 안에는 이미 AOP를 위한 기본 설정이 되어 있고 @Aspect 어노테이션을 사용한 class를 만들고 어노테이션을 정의해 권한 체크를 위임할 수 있다.

실제로 원하는대로 작동하는지 예측할 정도로 공부하진 않아 직접 사용하지는 못했지만 구조를 잡아보면 이렇게 어노테이션을 통해 인터셉트 한다는 느낌인 것 같다.

권한이 없는 유저에 대한 별도 처리가 필요한 게 아니면 꽤 유연하게 사용 가능해보인다.

@Aspect
@Component
class AuthorizationAspect(
    private val userService: UserService
) {

    @Before("@annotation(CheckPermission) && args(userId,..)")
    fun checkPermission(userId: Long) {
        val currentUser = userService.getCurrentUser()
        if (/*권한체크*/) {
            throw /*...*/
        }
    }
}

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class CheckPermission

// 서비스
@CheckPermission
fun createTodo(userId: Long) { /*...*/}

Spring security를 이용한 권한 체크

아직 Jwt를 써보지 않았기에 손도 못대본 Spring security에서도 역할 기반 접근 제어(Role-Based Access Control, RBAC) 를 지원하기 때문에 어플리케이션 전반적인 권한 체크를 진행할 수 있다고 한다.

이쪽은 Config부터 전반적인 설정의 규모가 달라 엄격해보여서 내 프로젝트에 적용하기는 꽤 어려워보인다. 엄격하기 때문에 더욱 조심해서 설정해야할 것 같다.

WebSecurityConfigurerAdapter를 상속받은 Config class가 이런 저런 어노테이션을 상속받고 구현해야하는 멤버(ex. userDetailsService)도 꽤 다양해서 이 쪽은 각잡고 주로 쓰는 부분을 공부해야할 것 같다. 도무지 정리할 수준이 안된다.

코드 중복이 많은 수동 방식, AOP를 이용한 방식 보다 인증, 인가에서 가장 강력한 보안을 자랑하는 Spring security를 결국 잘 써먹어야할 것 같다.

어플리케이션 전반적인 권한 체크자체가 신뢰가 가는 키워드고 보장이 되는 반면에 수동 체크, AOP 체크는 명확하지만 휴먼 에러의 위험성이 존재할 거라 본다.

내 과제와 해셜 세션 비교하기

이번 과제 해셜 세션은 실시간이 아닌 Step별로 녹화 영상이 제공됐다.
확실히 미숙한 영역의 Spring이라 그런지 놓친 부분이 많아서 비교하면서 또 기록해보기로 했다.

  • Put 대신 Patch를 적극 활용하셨다.
    Todo card 완료처리 같은 작은 기능은 Patch를 적용해보면 좋았을 것 같다.
  • 변경은 @Transactional 안에서 repository.save는 하지 않아도 되는 걸 몰랐다.
    Transactional 안에서는 마지막에 commit할 때까지 알아서 변경사항을 감지하고 flush한다. .save를 쓰면 명시적으로 저장 시점을 확실히 할 수 있지만 .save한 순간 Entity가 flush되므로 Transactional이 정상적으로 끝나지 않을 여지가 있으면 중간에 사용하는 건 주의해야 한다.
    기본적으로는 EntityManager에 위임하는 것이 더 안전할 것 같다.
  • Entitiy -> Dto 변환의 책임을 Dto가 가져갔다.
    나는 Dto가 Entity에 대한 정보를 몰라야할 것 같아서 DtoConverter를 따로 만들었는데 Dto에 작성하는 것도 생각 해봤지만 아직 확신을 갖고 결정한 건 아닌 느낌이다.
  • Swagger를 통한 실제 명세를 자주 확인해보지 않았다.
    빌드는 자주 해봤는데 Swagger는 자주 안해봐서 이번 과제에선 테스트가 많이 취약했다.
  • 나는 Entity가 가진 검증같은 책임을 많이 체크하진 못했다.
  • N+1을 방지하기 위해 JPA에만 의존하지 않고 Fetch join을 위한 JPQL도 적극 활용하셨다.
    이 방식으로 FetchType Lazy, Eager에만 의존하는 연관이 아닌 특정 로직에서만 Join을 이용할 수 있게 변경 가능하다.
    단점으로는 Fetch join과 Pagination을 함께 사용하면 쿼리가 사용하는 메모리가 커져 Out of memory가 발생할 수도 있다.
  • 인증/인가에 Jwt 토큰을 사용하셨다.

코드카타 - 프로그래머스 문자열 나누기

문자열 s가 입력되었을 때 다음 규칙을 따라서 이 문자열을 여러 문자열로 분해하려고 합니다.

  • 먼저 첫 글자를 읽습니다. 이 글자를 x라고 합시다.
  • 이제 이 문자열을 왼쪽에서 오른쪽으로 읽어나가면서, x와 x가 아닌 다른 글자들이 나온 횟수를 각각 셉니다. 처음으로 두 횟수가 같아지는 순간 멈추고, 지금까지 읽은 문자열을 분리합니다.
  • s에서 분리한 문자열을 빼고 남은 부분에 대해서 이 과정을 반복합니다. 남은 부분이 없다면 종료합니다.
  • 만약 두 횟수가 다른 상태에서 더 이상 읽을 글자가 없다면, 역시 지금까지 읽은 문자열을 분리하고, 종료합니다.

문자열 s가 매개변수로 주어질 때, 위 과정과 같이 문자열들로 분해하고, 분해한 문자열의 개수를 return 하는 함수 solution을 완성하세요.

문제 링크

class Solution {
    fun solution(s: String): Int {
        var answer: Int = 0
        var x = ' '
        var xCount = 0
        var notXCount = 0

        s.forEachIndexed { index, it ->
            val isLastIndex = index == s.length - 1 
            if (!isLastIndex && xCount == 0) {
                xCount++
                x = it
            } else {
                if (it == x) xCount++
                else notXCount++

                if (xCount == notXCount) {
                    answer++
                    xCount = 0
                    notXCount = 0
                } else if (isLastIndex) {
                    answer++
                }
            }
        }

        return answer
    }
}

효율 좋게 풀었다고 생각이 들긴 하는데 너무 C style 코드인게 마음에 안든다.

이번 문제는 문자열을 나누라고 되어있지만 실제로 s를 나누거나 빼가면서 풀어야 하는 문제는 아니고 쉽게 생각해서 나누는 횟수만 지정하면 되기 때문에 O(n)으로 s만 순회하고 answer를 이용하기로 했는데 그 과정에서 코드와 변수가 너무 많아졌다.

알고리즘적으로는 사실 어려울 것 없이
1. 첫번째 글자 x를 지정한다
2. 첫번째 글자가 아니면 x, notX를 계산하며 기록한다
3. 기록이 끝난 숫자가 같으면 문자열을 나눈 걸로 가정하고 횟수를 올린다.
4. 마지막 글자에서 문자열 계산이 안되면 횟수를 올린다.

시간 복잡도를 최소화 하는 방식으로만 접근해서 이렇게 됐는데 아무래도 Kotlin 스럽게 짠 부분은 전혀 없는 것 같은게 아쉬울 따름이다.

위안이라면 다른 코드중에서도 Kotlin 스럽게 짜보려는 코드보단 어쩔 수 없이 복잡한 조건이 많이 들어간 경우가 많았다.

그 중에서도 가장 깔끔한 코드를 골라오자면 Stack을 이용한 풀이가 가장 보기 좋았다.

fun solution2(s: String): Int {
        var answer: Int = 0

        val stack = mutableListOf<Char>()

        s.forEach { 
            if (stack.isEmpty()) {
                answer++
                stack.add(it)
            } else if (stack.first() == it) {
                stack.add(it)
            } else {
                stack.removeFirst()
            }
        }

        return answer
    }

이걸 보고 든 생각이 stack을 이용하면 count를 할 필요가 없어 보이고 문제가 stack 구조를 유도한 건가 싶다.

0개의 댓글