뉴스피드 프로젝트
AccessToken과 RefreshToken
| 항목 | 액세스 토큰 | 리프레시 토큰 |
| 목적 | 사용자 인증 후, 리소스 접근 권한 부여 | 액세스 토큰 만료 시, 새로운 토큰 발급 |
| 사용처 | API 요청 시 헤더에 포함 (Authorization) | 토큰 재발급 요청 시 사용 |
| 수명 | 짧음 (몇 분 ~ 수십 분) | 김 (수 시간 ~ 수 일) |
| 보안 위험 | 탈취 시 즉시 리소스 접근 가능 | 탈취 시 장기적인 접근 가능성 있음 |
| 저장 위치 | 클라이언트 메모리, 쿠키 등 | 보통 HttpOnly 쿠키 또는 보안 저장소 |
| 서버 검증 방식 | JWT의 경우 자체 검증 가능 | 보통 DB나 Redis에서 검증 필요 |
동작 흐름 예시
1. 로그인 성공 → 서버가 액세스 토큰 + 리프레시 토큰 발급
2. 클라이언트는 액세스 토큰을 이용해 API 요청
3. 액세스 토큰 만료 → 리프레시 토큰으로 새로운 액세스 토큰 요청
4. 리프레시 토큰도 만료 → 재로그인 필요
// 액세스 토큰 발급
public String createAccessToken(long userId) {
Date now = new Date();
return Jwts.builder()
.subject(String.valueOf(userId))
.issuedAt(now)
.expiration(new Date(now.getTime() + JwtConstants.ACCESS_TOKEN_EXPIRATION))
.signWith(SECRET_KEY, Jwts.SIG.HS256)
.compact();
}
`Jwts.buider()`를 통해 JWT 객체를 생성
`.subject()` 필드에 `userId`를 문자열로 변환하여 저장
`.issuedAt()` 토큰이 발급된 시간 설정
`.expiration()` 토큰의 만료 시간 설정
`.signWith()` 토큰을 `SECRET_KEY`로 서명, `HS256`은 HMAC-SHA256 알고리즘으로, 대칭키 기반 서명 방식
`.compact()` 설정한 모든 정보를 바탕으로 JWT 문자열을 생성하여 완성된 토큰 반환
위와 같이 사용자는 로그인할 때 액세스 토큰과 리프레시 토큰을 발급받는다. 프로젝트에는 액세스 토큰을 요청 헤더에 담아 인증을 처리하고, 리프레시 토큰은 쿠키에 저장함으로써 서버가 상태를 기억하지 않는 무상태(stateless) 구조를 유지하려 했다. 아래는 리프레시 토큰을 HttpOnly 쿠키로 설정하는 메서드이다.
public void addHttpOnlyCookie(HttpServletResponse response, String value) {
ResponseCookie cookie = ResponseCookie.from(JwtConstants.REFRESH_TOKEN, value)
.httpOnly(true)
.secure(true)
.path("/")
.sameSite("Strict")
.maxAge(JwtConstants.REFRESH_TOKEN_EXPIRATION / 1000)
.build();
response.addHeader("Set-Cookie", cookie.toString());
}
`ResponseCookie.from()` 쿠키 이름은 `REFRESH_TOKEN`, 값은 전달받은 토큰 문자열로 설정
`.httpOnly(true)` JavaScript에서 접근 불가능한 HttpOnly 쿠키로 설정 (XSS 공격 방지에 효과적)
`.secure(true)` HTTPS 환경에서만 쿠키가 전송되도록 설정 (네트워크 보안 강화)
`.path("/")` 쿠키가 모든 경로에서 유효하도록 설정
`.sameSite("Strict")` 타 도메인 요청에는 쿠키가 전송되지 않도록 설정 (CSRF 공격 방지)
`.maxAge()` 쿠키의 유효 시간을 설정 (리프레시 토큰 만료 시간에 맞춰 쿠키도 함께 만료되도록 설정)
`.build();` 설정한 쿠키 정보를 바탕으로 `ResponseCookie` 객체를 생성
`response.addHeader("Set-Cookie", cookie.toString());` 생성된 쿠키를 HTTP 응답 헤더에 추가
ResponseCookie
Spring에서 HTTP 응답에 쿠키를 설정할 때 사용하는 객체이다. 빌더 패턴을 사용해 쿠키의 속성을 한눈에 보기 쉽게 설정이 가능하다.
낙관적 락(Optimistic Lock)
낙관적 락은 동시성 제어를위한 전략 중 하나로, 특히 경합이 적은 환경에서 성능을 높이기 위해 사용되는 방식이다.
뉴스피드 프로젝트에선 게시글 좋아요 처럼 좋아요 수는 자주 바뀌지만 충돌 가능성은 낮은 상황이라 보면 되겠다.
그래서 낙관적 락은 데이터 충돌이 드물다고 가정하고, 먼저 데이터를 자유롭게 읽고 수정한 뒤 저장 시점에 충돌 여부를 검사하여 문제가 있으면 롤백하는 방식이다.
@Version
private Long version;
`@Version` 어노테이션이 붙은 필드는 JPA가 자동으로 관리한다. 엔티티를 수정할 때마다 `version`값이 증가하며, 저장 시점(트랙잭션이 끝날 때)에 버전이 다르면 `OptimisticLockException`이 발생한다. 이를 통해 좋아요 기능의 동시성 문제를 해결할 수 있었다.
비관적 락(Pessimistic Lock)
낙관적 락과는 반대되는 개념으로, 데이터 충돌 가능성이 높을 때 안정성을 확보하기 위해 사용하는 전략이다.
재고가 한정적일 때 여러 사용자가 동시에 상품을 주문할 수 있는 상황이나, 좌석 예약 시스템 같은 경우에도 많이 사용된다.
비관적 락은 여러 트랙잭션이 동시에 같은 데이터를 수정할 가능성이 있다고 가정하고, 먼저 락을 걸어 다른 트랜잭션의 접근을 차단하는 방식이다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Product findByIdForUpdate(@Param("id") Long id);
`@Lock` 어노테이션으로 락 모드를 지정하고, `PESSIMISTIC_WRITE`로 쓰기 락 설정 (다른 트랜잭션이 읽거나 쓰지 못함)
낙관적 락 vs 비관적 락
| 구분 | 낙관적 락 | 비관적 락 |
| 전략 | 충돌이 없다고 가정 | 충돌이 있을 수 있다고 가정 |
| 방식 | 버전 비교 | DB 락으로 접근 차단 |
| 성능 | 경합 적을 때 유리 | 경합 많을 때 안정적 |
| 예외 | 저장 시점에 충돌 검사 | 충돌 자체를 방지함 |
| 사용 예 | 프로필 수정, 게시글 추천 | 재고 관리, 금융 거래 |
대망의 프로젝트 발표

팀 프로젝트가 처음이라 같이 마음을 맞춰가는 과정이 굉장히 어렵게 느껴졌었다. 그래도 모두에게 더 좋은 결과를 만들고 싶은 욕심이 있었기에 조금씩 서로의 의견을 조율하며 진행했던 것 같다. 가장 기억나는 순간이 있는데, 바로 아직 배운 것도 얼마 없음에도 불구하고 어떤 부분에서 이 방법 외에는 방법이 없다고 불가능하다는 생각을 가졌던 순간이다. “어떻게 구현할 수 있을까”가 아니라 어차피 이건 안될 거라는 편협한 생각을 하는 저를 보고 굉장히 놀랐다. 아마 팀원들과 함께 하는 작업이 아니었다면 계속 모르지 않았을까 하는 생각도 든다. 이 프로젝트를 통해 굉장히 중요한 것을 배웠다고 느꼈다. 팀원들이 발표 당일 모두 참석하지는 못 했지만 결과물이 퇴색되지 않도록 최선을 다했다. 열심히 준비했음에도 항상 사람들 앞에 서는 일은 떨린다. 그래도 괴로운 만큼 성장한다는 믿음으로 꾸준히 나를 어려움에 던져 성장시켜야겠다고 생각했다. 끝나고나니 아쉬움도 후련함도 있지만 다음 프로젝트에 대한 기대도 크다. 이번 경험을 바탕으로 더 좋은 결과물로 나의 성장을 증명하고 싶다.