5/11(월)

dev_joo·어제

새 다짐

오늘부터 부트캠프과정이 2주일밖에 남지 않았다. (주 5일이니까 딱 10일!)
그래서 오늘부터는 과정 마무리에 집중하면서 매일 9시까지 빠이팅🙌(🤦🏻‍♀️)

코드 카타

푸드 파이트 대회

음식 insert 하기

class Solution {
    public String solution(int[] food) {
        // 참여 선수 2, 음식 대칭 배치
        // 홀수개면 -1 개 사용
        // 사용 되는 개수/2 개 배치, 대칭 배치
        // 대칭 배치는 좌우반전 또는  동시에 양쪽 배치
        // 동시에 양 쪽 배치가 시간 복잡도 적을 것 같음.
        
        // 가운데 들어갈 물(0) 초기화
        StringBuilder sb = new StringBuilder("0"); 

        for (int i = food.length - 1; i > 0; i--) {
            for (int j = 0; j < food[i] / 2; j++) {
                sb.insert(0, i);
                sb.append(i); 
            }
        }
        return sb.toString();
    }
}

하지만 StringBuilder.insert()를 반복해서 사용하면 시간 복잡도가 오히려 증가한다.
문자열 중간에 값을 끼워 넣을 때마다 그 뒤의 문자들을 모두 뒤로 밀어내야 하기 때문이다.
삽입할 때마다 O(N)O(N)의 시간이 걸려, 결과적으로 O(N2)O(N^2)의 시간 복잡도를 가지게 된다.

음식 반전 시키기

반면 reverse()O(N)O(N)이 걸리기 때문에, 전체 로직이 O(N)O(N)의 시간 복잡도로 처리될 수 있다.

class Solution {
    public String solution(int[] food) {
       StringBuilder sb = new StringBuilder(); 

        // 왼쪽 선수가 먹을 음식 배치 (음식 번호 1번부터 순차적으로)
        for (int i = 1; i < food.length; i++) {
            for (int j = 0; j < food[i] / 2; j++) {
                sb.append(i); 
            }
        }
        String leftSide = sb.toString();
        sb.append(0); // 가운데 물 추가
        sb.append(new StringBuilder(leftSide).reverse());
        return sb.toString();
    }
}

앞으로는 각 메서드의 시간복잡도를 생각하며 사용해야겠다.

배열로 양쪽에 동시 배치

배열을 사용하면, 기존 아이디어를 살리면서도 시간 복잡도를 줄일 수 있다.

class Solution {
    public String solution(int[] food) {
        int totalLength = 1; 
        for (int i = 1; i < food.length; i++) {
            totalLength += (food[i] / 2) * 2;
        }

        char[] result = new char[totalLength];
        
        // 양 끝 인덱스 
        int left = 0;
        int right = totalLength - 1;
        
        // 중앙에 물 배치
        result[totalLength / 2] = '0';

        // 양방향으로 음식 채우기
        for (int i = 1; i < food.length; i++) {
            int count = food[i] / 2;
            char foodChar = (char) (i + '0');
            
            for (int j = 0; j < count; j++) {
                result[left++] = foodChar;
                result[right--] = foodChar;
            }
        }

        return new String(result);
    }
}

인증 방식 비교

1. JWT 의 본질

JWT(JSON Web Token)는 대표적인 Stateless 인증 방식이다.

1.1 JWT의 특징:

  • 서버가 세션 상태를 저장하지 않음
  • 토큰 자체에 인증 정보 포함 (Self-contained= 토큰 안에 필요한 인증 정보가 들어있음)
{
  "sub": "user123",
  "role": "ADMIN",
  "exp": 1710000000
}
  • 서명 검증만으로 신뢰 가능
  • 분산 환경 / MSA 에 매우 적합

1.2 JWT Stateless 인증 방식의 장점:

  1. 서버 메모리 사용 감소
    • 세션 데이터(사용자 정보)를 서버에 보관할 필요가 없다. 서버는 토큰의 유효성만 검증하면 되므로, 동시 접속자가 많아도 메모리 부하가 거의 없다.
  2. 수평 확장 쉬움
    • 유저 정보가 토큰에 담겨 있어 어느 서버로 요청을 보내도 인증이 가능하다. 세션 공유를 위한 별도의 복잡한 설정 없이 서버 대수를 자유롭게 늘릴 수 있다.
  3. Gateway 단 검증 가능
    • 여러 서비스로 구성된 MSA 환경에서, 시스템 관문(Gateway)이 토큰을 한 번만 검증하면 하위 서비스들은 별도 인증 없이 정보를 믿고 사용할 수 있어 효율적이다.
  4. 세션 저장소 의존 감소
    • 인증을 위해 Redis나 DB 같은 외부 저장소를 매번 조회(Network I/O)할 필요가 없다. 토큰 그 자체로 인증이 완료되므로 시스템 독립성이 높아지고 속도가 빠르다.

2. JWT Stateless 인증 방식의 약점

2.1 구조적 한계

"발급된 토큰을 서버가 즉시 통제하기 어렵다"

로그아웃 즉시 반영 어려움

권한 변경 즉시 반영 어려움

탈취 시 만료 전까지 사용 가능

비밀번호 변경 후 기존 토큰 유지 가능

2.2 시나리오별 위협

시나리오상황문제점
로그아웃 문제1. 사용자 로그인
  1. JWT 발급
  2. 사용자가 로그아웃
  3. 탈취된 JWT 는 여전히 만료 전까지 사용 가능‼️ | JWT 자체는 이미 유효한 서명을 가짐서버가 "이 토큰은 폐기됨" 을 알 방법 없음 |
    | 권한 변경 문제 | JWT 내부에 role=ADMIN 포함이후 ADMIN → USER 권한 변경 | 기존 JWT 의 role=ADMIN 유지만료 전까지 관리자 권한 사용 가능 |
    | RefreshToken 탈취 문제 | 공격자가 RT를 탈취 | RT를 이용해 새 AT 무한 발급 가능
    RT 만료가 길수록 위험Rotation 없으면 장기 악용 가능 |
    | 비밀번호 변경 문제 | 사용자가 비밀번호 변경 | 기존 JWT 는 여전히 유효모든 디바이스 강제 로그아웃 어려움 |
    | 다중 디바이스 문제 | 모바일 / 태블릿 / PC 동시 로그인 환경 | 특정 디바이스 로그아웃모든 디바이스 로그아웃디바이스별 RT 관리 필요 |

3. 구조적 대응 (하이브리드 방식)

모든 기술은 AT, RT에 교차 적용이 가능하나, 시스템의 처리량과 보안 강도를 고려하여 활용 범위를 정함.

3.1 AccessToken 관리 : 실시간 무효화

전략설명장점단점/한계
블랙리스트로그아웃/정지된 토큰 ID를 Redis에 저장하여 차단

대부분의 정상 유저는 리스트에 없으므로 최소한의 확인
즉각적인 통제권 회수. 특정 토큰만 선택적으로 차단 가능Stateful 의존성
토큰 버전 관리유저 정보에 버전 번호를 부여하고 토큰 내 버전과 대조일괄 로그아웃 가능. 모든 기기의 접속을 한 번에 끊을 때 유리특정 기기만 골라서 끄는 세밀한 제어 불가능

3.2 RefreshToken 관리

전략설명장점단점
화이트리스트서버가 발급한 RT 목록만 DB에 저장하여 관리

모든 요청마다 저장소(State)를 조회해야 하므로, 블랙리스트 방식보다 서버 부하가 큼
서버의 완전한 통제. 허가되지 않은 갱신 시도를 원천 차단Stateful 의존성
RTR (Rotation)RT 사용 시마다 새 RT를 발급하고 기존 것은 폐기탈취 즉시 감지. 해커와 유저의 토큰 충돌을 통해 침입 확인 가능네트워크 순단 시 정상 유저도 로그아웃될 수 있는 UX 리스크

3.3 인증 방식 요청 흐름 비교

[1. 전통적 세션 방식] - 매 요청마다 DB(세션 저장소) 필수 방문
사용자 -> [API 서버] -> [Session DB (Redis/RDB)] -> 인증 완료 -> 결과 반환
           (SessionID 전송)     (매번 Read/Write 발생)
          
---------------------------------------------------------------------

[2. 순수 JWT 방식] - DB 방문 없음 (Stateless, 제어 불가)
사용자 -> [API 서버] (내부 서명 검증만 수행) -> 인증 완료 -> 결과 반환
           (JWT 전송)      (DB 조회 0)
           * 단점: 로그아웃을 해도 만료 전까지는 토큰이 살아있음
           
---------------------------------------------------------------------

[3. Pagely 하이브리드 방식] - 성능과 보안의 절충안
A. 일반 API 요청 (대부분의 케이스)
사용자 -> [API 서버] -> [Redis (블랙리스트 체크)] -> 인증 완료 -> 결과 반환
           (AT 전송)     (차단된 유저만 확인, 매우 빠름)

B. 토큰 갱신 요청 (RT 사용 시)
사용자 -> [API 서버] -> [Redis (화이트리스트+RTR)] -> 새 토큰 발급 -> 결과 반환
           (RT 전송)     (저장된 RT와 1:1 대조 및 교체)

3.4 하이브리드 방식의 의미

위와 같은 구조적 보완책을 도입하면 일부 Stateful해지는 측면이 있지만, 로그아웃이나 RT 갱신과 같은 시점의 민감한 일부 요청일 때만 상태를 확인하기 때문에 세션 방식 수준의 보안성을 확보할 수 있다. (보안)

블랙리스트 / 화이트리스트 도입 시 Redis 조회가 추가되어 검증 비용이 증가한다. 하지만 JWT 의 진짜 가치인 Self-contained 은 유지된다. 모든 API 요청에 대해서 세션을 저장한 저장소를 한 번씩 읽어야하는 세션 방식과 비교했을 때, 한 번 발급받은 AT의 자기 완결적 데이터를 활용하기 때문에 대부분의 요청을 DB의존성 없이 빠르게 처리한다. (성능)

  • 권한 변경 이벤트 발생 시 Auth Service Consumer 가 해당 사용자의 RT 제거,
    AT 블랙리스트 등록을 수행

  • AT 는 요청 빈도가 높아서 성능 중심 AT = 블랙리스트

  • RT 는 빈도 낮고 보안 중요도가 높기 때문에 RT = 화이트리스트


Redis 인프라 설정

profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글