Gin으로 API 서버를 옮겼다면,
다음으로 거의 반드시 등장하는 요구사항이 인증(Authentication)과 인가(Authorization) 다.
이번 챕터에서는
세션 기반이 아니라 JWT(Json Web Token) 를 사용하는 방식으로
- 인증 흐름이 어떻게 구성되는지
- Gin 미들웨어로 어떻게 분리하는지
- 실무에서 자주 실수하는 포인트는 무엇인지
를 차분하게 정리한다.
“완벽한 보안 구현”보다는
API 서버에서 반복적으로 쓰이는 기본 설계를 이해하는 게 목표다.
인증과 인가는 구분해서 생각해야 한다
먼저 용어부터 정리하는 게 좋다.
- 인증(Authentication): 너 누구냐
- 인가(Authorization): 이 작업 해도 되냐
JWT는 주로
- 인증 결과를 토큰으로 전달하고
- 이후 요청에서 인가 판단의 근거로 사용
하는 데 쓰인다.
왜 JWT를 사용하는가
JWT가 많이 쓰이는 이유는 비교적 명확하다.
- 서버가 세션 상태를 들고 있지 않아도 된다
- API 서버 / 모바일 / 프론트엔드 분리 구조에 적합
- 토큰 하나로 인증 정보 전달 가능
특히 Go + API 서버 조합에서는
상태 없는(stateless) 구조와 잘 맞는다.
JWT 인증의 전체 흐름
JWT 기반 인증 흐름은 보통 다음과 같다.
- 사용자 로그인 (ID / 비밀번호)
- 서버에서 JWT 생성
- 클라이언트가 토큰 저장
- 이후 요청마다 토큰 전송
- 서버에서 토큰 검증 후 요청 처리
[이미지: JWT 인증 전체 흐름]
이 흐름을 코드 구조로 옮기는 게 이번 챕터의 핵심이다.
토큰을 어디에 담아 보낼까
API 서버에서는 보통
Authorization 헤더를 사용한다.
Authorization: Bearer <JWT_TOKEN>
이 방식의 장점은
- HTTP 표준에 가깝고
- 프록시, 미들웨어와 잘 어울리며
- 쿠키보다 의도가 명확하다
그래서 이 글에서도
Authorization 헤더 기준으로 설명한다.
JWT에 담을 정보는 최소한으로
JWT payload에는 여러 정보를 담을 수 있지만,
실무 기준으로는 다음 정도가 적당하다.
- 사용자 ID
- 토큰 만료 시간(exp)
- 발급자(iss)
{
"user_id": 42,
"exp": 1700000000
}
⚠️ 주의할 점
- 민감 정보(비밀번호, 권한 전체 목록)는 넣지 않는다
- JWT는 암호화가 아니라 서명이다
- 토큰 내용은 누구나 디코딩할 수 있다
JWT 생성 위치는 어디가 좋을까
JWT 생성은 보통 다음 위치 중 하나다.
- 인증용 service
- 인증 전용 패키지
중요한 기준은 이것이다.
handler에서 직접 JWT를 만들지 않는다
handler → service → token 생성
이 흐름을 지키면
테스트와 유지보수가 훨씬 쉬워진다.
로그인 API 예시 흐름
POST /login
- handler에서 요청 파싱
- service에서 사용자 검증
- JWT 생성
- 토큰 응답
type LoginResponse struct {
Token string `json:"token"`
}
이 API는
JWT를 발급하는 유일한 진입점이 된다.
Gin 인증 미들웨어 기본 형태
이제 핵심인 인증 미들웨어를 보자.
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
auth := c.GetHeader("Authorization")
if auth == "" {
c.AbortWithStatus(401)
return
}
// 토큰 파싱 및 검증
userID, err := parseToken(auth)
if err != nil {
c.AbortWithStatus(401)
return
}
c.Set("userID", userID)
c.Next()
}
}
이 코드의 역할은 명확하다.
- 토큰이 없으면 차단
- 토큰이 유효하지 않으면 차단
- 유효하면 사용자 정보 저장 후 다음으로 진행
[이미지: Gin 인증 미들웨어 동작 흐름]
인증 정보는 context에 담는다
미들웨어에서 인증이 끝났다면,
그 결과를 handler로 전달해야 한다.
Gin에서는 Context를 사용한다.
c.Set("userID", userID)
handler에서는 이렇게 꺼낸다.
userID := c.GetInt("userID")
⚠️ 주의할 점
- service 계층으로 *gin.Context를 넘기지 않는다
- handler에서 필요한 값만 추출해서 전달한다
이 기준이 무너지면
프레임워크 의존이 빠르게 퍼진다.
인증이 필요한 API에만 적용하기
Gin에서는 미들웨어 적용 범위를 쉽게 제어할 수 있다.
api := r.Group("/api")
api.Use(AuthMiddleware())
{
api.GET("/users/me", handler.Me)
}
- 로그인, 회원가입 API → 미들웨어 미적용
- 보호된 API → 미들웨어 적용
이 구조가 명확하면
“이 API는 인증이 필요한가?”를 한눈에 파악할 수 있다.
인가(Authorization)는 어디서 처리할까
인가 판단은 보통 다음 기준으로 나뉜다.
- 단순 권한 체크 → handler
- 비즈니스 규칙 기반 → service
예를 들어
- “본인 정보만 조회 가능”
- “관리자만 삭제 가능”
같은 규칙은
service 계층에서 판단하는 게 자연스럽다.
JWT는
인가 판단을 위한 힌트를 제공할 뿐,
모든 결정을 대신해주지는 않는다.
JWT 사용 시 자주 나오는 실수
⚠️ 실무에서 정말 자주 보는 실수들
- 토큰 만료(exp) 체크 누락
- HTTPS 없이 토큰 전달
- 로그에 토큰 전체 출력
- JWT를 세션처럼 사용하는 설계
JWT는 편리하지만,
한 번 유출되면 만료 전까지는 무효화가 어렵다는 특성이 있다.
그래서 만료 시간 설정은 사실상 필수다.
이 구조의 장점
이 챕터에서 만든 구조의 장점은 다음과 같다.
- 인증 로직이 미들웨어에 집중됨
- handler / service 구조 유지
- 테스트 시 인증 미들웨어만 분리 가능
즉,
보안 로직이 코드 전체에 흩어지지 않는다.
정리
JWT 기반 인증의 핵심은
토큰 자체보다 구조와 책임 분리다.
- 로그인 API에서만 토큰 발급
- 인증은 미들웨어에서 처리
- 인가는 service 중심으로 판단
- Gin Context는 handler까지만 사용
이 구조를 잡아두면,
이후에
- 권한(Role) 추가
- Refresh Token 도입
- OAuth 연동
같은 확장도 훨씬 수월해진다.
다음 챕터로는
- PostgreSQL 연동 + migration 설계
- API 에러 응답 규약 통일하기
- 실무용 로그 / 에러 / 트레이싱 정리
중 하나로 이어가면 흐름이 좋다.
'backend' 카테고리의 다른 글
| Go에서 PostgreSQL 연동하기: 실무 기준 DB 연결과 Migration 설계 (0) | 2026.02.05 |
|---|---|
| Go API 에러 응답 규약 정리: 실무에서 흔들리지 않는 기준 만들기 (1) | 2026.02.04 |
| Gin으로 API 서버 옮기기: 기존 net/http 구조 그대로 활용하기 (0) | 2026.02.02 |
| Go에서 DB 연동하기: database/sql과 Repository 패턴으로 구조 잡기 (0) | 2026.02.01 |
| Go로 간단한 CRUD API 만들기: REST 구조와 테스트 코드까지 연결하기 (1) | 2026.01.31 |