BindProject

[Backend] JWT 토큰 모듈 설계 및 구현기

dding-shark 2025. 6. 17. 23:44
728x90

JWT 토큰 모듈 설계 및 구현기

도입

MSA 구조에서 인증/인가 처리의 핵심은 토큰 기반 인증이다.
우리는 BFF가 직접 토큰 발급/검증 로직을 갖지 않도록 하기 위해 public-token이라는 전용 모듈을 만들어 책임을 분리했다.
이 글에서는 JWT 발급 및 검증을 위한 모듈을 설계하고 테스트한 과정을 공유한다.


설계 목표

  • JWT 발급/검증 기능을 재사용 가능한 공용 모듈로 구성
  • BFF, Auth 등 다양한 서비스에서 쉽게 DI하여 사용 가능하게 구성
  • .env 또는 application.yml을 통해 시크릿 키와 만료 시간 등 설정값을 외부화
  • WebFlux 환경에서도 간단히 연동 가능하도록 설계

설계 고려사항

  • 키 보안: 서명 키는 외부 설정으로 주입받아야 하며, 256bit 이상을 권장
  • 만료 처리: 토큰 유효 기간 설정은 환경에 따라 유연해야 함
  • Claims 설계: 최소한 subject를 기반으로 식별 가능해야 하며, 확장을 고려하여 Map 기반 claims 구조 사용
  • 서명 알고리즘: JJWT에서 기본 제공하는 HMAC-SHA256 알고리즘 사용
  • Spring 의존도 낮춤: POJO 기반으로 설계하여 테스트가 쉽고 모듈 분리가 가능하게 함

구현 설명

JwtTokenProvider

토큰 발급, 검증, Claims 파싱 기능을 담당하는 클래스이다.

public class JwtTokenProvider {
    public String issueToken(String subject, Map<String, Object> claims);
    public Claims parse(String token);
    public boolean isValid(String token);
    public String getSubject(String token);
}

TokenConfig

Spring Boot에서 설정값을 바인딩하고 JwtTokenProvider Bean을 생성한다.

@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class TokenConfig {
    @Bean
    public JwtTokenProvider jwtTokenProvider(JwtProperties props) {
        return new JwtTokenProvider(props.getSecret(), props.getExpiration());
    }
}

JwtProperties

application.yml 또는 .env에서 설정을 받아오는 용도다.

jwt:
  secret: ${TOKEN_KEY:dev-secret}
  expiration: ${TOKEN_EXPIRATION:3600}

테스트 전략

단위 테스트는 다음 항목에 대해 작성되었다.

  • 토큰 발급 후 subject 및 claims 파싱 테스트
  • 유효한 토큰과 위조된 토큰에 대한 isValid 테스트
  • 만료 시간 테스트 (추가 예정) 

 


class JwtTokenProviderTest {

    private JwtTokenProvider tokenProvider;

    @BeforeEach
    void setUp() {
        String secret = "mysecretkeymysecretkeymysecretkey"; // 최소 256bit (32자리 이상)
        long expiration = 3600; // 1시간
        tokenProvider = new JwtTokenProvider(secret, expiration);
    }

    @Test
    void 토큰_정상_생성_및_파싱() {
        // given
        String subject = "user-123";
        Map<String, Object> claims = new HashMap<>();
        claims.put("role", "USER");

        // when
        String token = tokenProvider.issueToken(subject, claims);
        Claims parsed = tokenProvider.parse(token);

        // then
        assertThat(parsed.getSubject()).isEqualTo(subject);
        assertThat(parsed.get("role")).isEqualTo("USER");
    }

    @Test
    void 토큰_유효성_검사() {
        String token = tokenProvider.issueToken("user-456", Collections.emptyMap());

        assertThat(tokenProvider.isValid(token)).isTrue();
        assertThat(tokenProvider.isValid(token + "tampered")).isFalse(); // 위조된 토큰
    }

    @Test
    void 토큰에서_subject_추출() {
        String token = tokenProvider.issueToken("user-789", Collections.emptyMap());

        String subject = tokenProvider.getSubject(token);
        assertThat(subject).isEqualTo("user-789");
    }
}

 

테스트는 POJO 기반 구조 덕분에 Spring 없이 순수하게 JUnit5 + AssertJ로 작성했다.


회고 및 확장 가능성

만족한 점

  • 모듈 분리로 인증 로직을 각 서비스에서 재사용 가능하게 되었다
  • 설정값 외부화로 환경에 따라 키, 만료시간 등을 쉽게 교체 가능
  • 테스트 가능한 구조를 확보하여 추후 토큰 유형 변경에도 유연함

향후 확장 아이디어

  • Refresh Token 기능 추가 및 Redis 연동 구조 도입
  • access, refresh, admin 등 용도별 토큰 분리
  • 인증 정책 로깅 및 메트릭 (e.g., 발급 실패율, 만료 추적 등)

BFF와 인증 서비스가 깔끔하게 토큰 로직을 위임할 수 있는 구조가 완성되었고, 이 구조는 이후 Outbox, Kafka 이벤트 흐름과도 자연스럽게 연결될 수 있다.

728x90