최근 프로젝트에 JWT를 통한 인증/인가와 로그아웃 처리를 위해 Redis를 사용하여 회원 관리 기능을 구현했다.
그러다 선생님의 조언으로 어댑터 패을 알게 되었고, 바로 도입을 시도했다.
어댑터 패턴이란?
클래스가 어댑터로 사용되는 패턴을 말한다. 어댑터는 서로 호환되지 않는 단자를 호환시켜 작동하게끔 한다. 객체 지향 프로그래밍 관점에서 본다면, 호환성이 없는 인터페이스들로도 동작할 수 있게 변환해주는 것이 어댑터 클래스이다.
Flow
Service → RefreshTokenRepository (인터페이스)
↑
|
┌───────────┴───────────┐
| |
JpaAdapter RedisAdapter
| |
JpaRepository RedisTemplate
여기서 JpaAdapter와 RedisAdapter가 어댑터 역할을 할 것이다.
서비스 클래스는 RefreshTokenRepository만 알면 기능을 제대로 동작시킬 수 있기 때문에 결합도가 감소한다.
또한 RefreshToken 객체를 직접 사용하지 않고, RefreshDto를 사용해 구현할 것이다.
JpaAdapter는 RefreshToken 객체를 RefreshDto로 변환하여 값을 넘겨줄 것이고, 받은 RefreshDto는 RefreshDto로 변환하여 값을 저장, 삭제 등을 구현할 것이다.
RedisAdapter도 JpaAdapter와 마찬가지로 Redis 데이터를 RefreshDto로, RefreshDto를 Redis 데이터로 변환한다.
MySQL (Users 테이블)
- 회원가입
- 로그인 인증 (아이디/비밀번호 검증)
- 사용자 정보 영구 저장
Redis
- Refresh Token 관리 (화이트리스트)
- Access Token 블랙리스트 (로그아웃 시)
어댑터 패턴 구현
RefreshTokenRepository
public interface RefreshTokenRepository{
// Refresh Token 저장 (화이트리스트 추가)
void save(String token, Users user, long ttl);
// Token으로 조회
Optional<RefreshDto> findByToken(String token);
// Token 삭제 (로그아웃)
void deleteByToken(String token);
}
RefreshTokenRepository는 어댑터들이 구현해야할 메서드를 가지고 있다. 서비스 클래스는 이 인터페이스 하나만 알아도 RefreshToken 관련 비지니스 로직을 처리할 수 있다.
- ttl : 초 단위, 리프레시 유효 기간
JpaRefreshTokenRepositoryAdapter
@Repository
@Profile("mysql")
@RequiredArgsConstructor
public class JpaRefreshTokenRepositoryAdapter implements RefreshTokenRepository {
private final RefreshTokenJpaRepository jpaRepository;
@Override
public void save(String token, Users user, long ttl) {
RefreshToken refreshToken = RefreshToken.builder()
.token(token)
.user(user)
.expiryDatetime(LocalDateTime.now().plusSeconds(ttl))
.build();
jpaRepository.save(refreshToken);
}
@Override
public Optional<RefreshDto> findByToken(String token) {
return jpaRepository.findByToken(token)
.map(this::toDto);
}
@Override
@Transactional
public void deleteByToken(String refreshToken) {
jpaRepository.findByToken(refreshToken)
.ifPresent(token -> jpaRepository.deleteByToken(token.getToken()));
}
// Entity → DTO 변환
private RefreshDto toDto(RefreshToken refreshToken) {
return RefreshDto.builder()
.token(refreshToken.getToken())
.expiryDatetime(refreshToken.getExpiryDatetime())
.build();
}
}
Mysql 데이터베이스와 직접 연결되는 인터페이스를 구현하였다. 클래스이기 때문에 repository로 인식되지 않기 때문에 어노테이션을 수동으로 붙여주었다. findByToken() 메서드에서는 JpaRepository에서 찾은 RefreshToken 객체를 DTO로 변환하여 필요한 데이터만 반환한다.
RefreshTokenJpaRepository
public interface RefreshTokenJpaRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByToken(String token);
void deleteByToken(String token);
}
JPA를 상속 받아 구현되었다. save 메서드는 JPA에서 제공하기 때문에 커스텀 메서드만 구현하였다.
RedisRefreshTokenRepositoryAdapter
@Repository
@Profile("redis")
@RequiredArgsConstructor
public class RedisRefreshTokenRepositoryAdapter implements RefreshTokenRepository {
private final RedisTemplate<String, String> redisTemplate;
private static final String TOKEN_PREFIX = "refresh_token:";
@Override
public void save(String token, Users user, long ttl) {
String tokenKey = TOKEN_PREFIX + token;
redisTemplate.opsForValue().set(
tokenKey,
user.getId().toString(),
ttl,
TimeUnit.SECONDS
);
}
@Override
public Optional<RefreshDto> findByToken(String token) {
String tokenKey = TOKEN_PREFIX + token;
long ttl = redisTemplate.getExpire(tokenKey, TimeUnit.SECONDS);
if (ttl < 0) {
return Optional.empty();
}
String userId = redisTemplate.opsForValue().get(tokenKey);
if (userId == null) {
return Optional.empty();
}
LocalDateTime expiryDatetime = LocalDateTime.now().plusSeconds(ttl);
return Optional.of(RefreshDto.builder()
.token(token)
.expiryDatetime(expiryDatetime)
.build());
}
@Override
public void deleteByToken(String token) {
String tokenKey = TOKEN_PREFIX + token;
redisTemplate.delete(tokenKey);
}
}
Redis 에서 데이터를 받아오기 때문에 RedisTemplate을 선언한다.
(어댑터 패턴과는 상관 없는 내용이지만) Redis에 값을 저장하게 되면 값 자체만 저장이 된다. 따라서 어떤 값인지 알기 위해 PREFIX를 사용하였다.

어댑터 패턴의 장점
- 결합도 감소
- 서비스가 구현체에 의존하지 않음
- 오직 인터페이스에만 의존하게 됨
- Service에서 직접 Redis 문법을 사용하고 있다는 문제점 개선
- 저장소를 변경하더라도 서비스 로직에는 문제 X
- 환경별로 다른 저장소를 사용할 수 있음
- 테스트, 유지보수 용이

감사합니다 ( •̀ ω •́ )
'Development > Back-end' 카테고리의 다른 글
| [NestJS] 네이버 뉴스 카테고리 지정하기 (1) (0) | 2026.03.28 |
|---|---|
| [NestJS] Pipe 사용하기 (1) | 2026.02.26 |
| [Spring Boot] @Data 알고 쓰기 (0) | 2025.11.26 |
| [Spring Boot] Spring Security + JWT + Redis 로그아웃 구현하기 (0) | 2025.11.12 |
| [Spring Boot] JWT 서명 알고리즘 (2) - HS256 (0) | 2025.08.18 |
