배경
회원가입 시 카카오 인증만 진행하고 필수 정보를 작성하지 않은 사용자는 사용자가 맞을까?? 일단 내 기준에서는 아니다
필수로 받아야 한다고 정책으로 정해진 것이고 해당 값들이 있어야 저장을 시켜준다라고 정해진 이상 해당 값들이 없다면 그건 사용자가 아닌 것이고 저장을 해주면 안된다 라는 정책과 그럼 카카오로 부터 받은 값들을 필수 값이 들어오기 전까지 어디에 안전히 가지고 있을 지 문제에서 시작되었다
대안
1. 서버 내부 메모리에 저장
가장 간단한 방법이다 추가로 외부 리소스를 사용할 필요도 없고 내부에서 Map 형태로 우리가 만든 key 값, 사용자 정보 value로 저장을 한 후 해당 key 값을 제공하여 필수 정보 값과 같이 요청에 담아 보내도록 하면 된다
하지만 단점으로는 서버가 증가한다면?!?!? (답이 없군..) 각 서버마다 내부 메모리가 다르고 카카오 인증 이후 다른 서버로 요청이 간다면? 알 수 있는 방법이 없다
2. DB 내 다른 테이블에 저장
이 방법도 PK와 사용자의 정보를 담을 또 다른 컬럼을 가지면 위 방식의 Map과 동일한 효과를 만들어 낼 수 있다
Json 데이터를 직렬화 해서 저장하면 되니 굳이 여러 컬럼을 만들 필요도 없고 추후 가져와서 역직렬화를 해 사용하면 그만이다
하지만 역시 이 방법에서도 고민이 되던 단점은 임시의 사용자 데이터가 계속해서 쌓인다는 것이다
가입 완료 시 지워주면 그만 아니냐라는 답도 있을 수 있겠지만 카카오 인증만 하고 추가 정보 창에서 귀찮다고 나가버린 사용자는?!? 물론 스케줄링을 통해 특정 기간에 다 지워 줄 수 있겠지만 지금 들어온 값인지 아닌지에 대해 판별하는 과정이 필요하기 때문에 번거로운 방법이다
3. Redis에 저장
마지막으로 고려한 Redis는 위 모든 단점들을 해결해 줄 수 있다
외부 Redis를 사용하기 때문에 1번 서버가 여러 대여도 아무런 문제가 없고 Redis는 ttl을 설정해서 특정 시간이 지나면 자동으로 특정 시간이 지나면 데이터를 지워 준다
또 Redis의 key값을 null로 지정을 하면 자동으로 랜덤한 key값을 제공해 줘서 사용자에게는 암호화 된 코드와 같이 보여 줄 수 있다ㅋㅋㅋ
이러한 모든 문제를 해결 해 주기 때문에 소셜 로그인 과정 후 필수 정보를 받기 위해 필요한 시간 동안 Redis에 값을 저장해 두기로 결정을 했다
Redis 적용
처음에는 Redis를 사용하면서 또 문법을 공부해야하나 했다
하지만 역시 Spring Data
몇개의 설정만 추가하면 바로 추상화된 Repository를 사용해서 쉽게 접근 할 수 있게 되어있었다
설정 파일
@Configuration
@EnableRedisRepositories
public class RedisConfiguration {
@Bean
public RedisConnectionFactory redisConnectionFactory(RedisProperties properties) {
return new LettuceConnectionFactory(properties.getHost(), properties.getPort());
}
@Bean
public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
JPA를 사용해봤다는 가정하에 비교를 해보면 RedisConnectionFactory → EntityManagerFactory로 RedisTemplate → EntityManager로 매칭을 하면 이해가 쉽게 될 것이다
사용 예시
@RedisHash("people", timeToLive = 300)
public class Person {
private final String id;
private final String provider;
private final String nickname;
private final String email;
private final String imageUrl;
}
public interface PersonRepository extends CrudRepository<Person, String> {
}
기본적인 CRUD는 위 Repository를 사용하면 충분하고 추가적인 작업들이 필요하다면 아래와 같이 template를 사용하여 각 data 타입 마다 기능들을 사용 할 수 있다
RedisTemplate.opsForValue();
RedisTemplate.opsForStream();
RedisTemplate.opsForList();
Trouble Shooting
- RedisRepository Bean 찾지 못하는 문제
기본적으로 테스트를 할 때 팀 규칙으로 단위 테스트로 진행을 하기로 했고 항상 사용했던 것 처럼 @DataJpaTest
를 사용해서 테스트 진행했고 RedisRepository Bean을 받아오지 못하는 문제 발생했다
@DataJpaTest
대신 @DataRedisTest
를 사용해서 RedisRepository Bean을 받아오도록 변경했다
- 팀원들간 로컬 환경, CI/CD 환경 차이
테스트 시 로컬에 Redis를 다운받아서 사용을 해서 모든 팀원들이 로컬에 직접 다운을 받아야 하는 문제가 생겼고 우선은 Docker를 사용해서 Redis를 다운 받아 사용하도록 했다
docker pull redis
docker run --name resumeme-redis -p 6379:6379 redis
docker exec -it resumeme-redis /bin/bash
redis-cli
하지만 Github Action을 통해서 CI/CD를 진행을 할 때도 docker를 통해 redis를 매번 다운을 받아야 하는 문제가 있고 우선은 @Disabled
를 통해 테스트를 임시로 막아 두었고 Test Container로 변경하도록 결정을 했다
Test Container 적용
기본적으로 도커는 설치가 되어있어야 한다
Github Action에서는 도커는 설치가 되어있고 Workflow를 실행 시키기 위해서 따로 필요한 과정은 없다
아래와 같이 TestContainer를 등록하면 된다
이제 단순히 docker만 실행이 되고 있으면 알아서 redis를 실행시켜서 테스트를 진행하게 된다
@DataRedisTest
@Testcontainers
class OAuth2InfoRedisRepositoryTest {
@Autowired
private OAuth2InfoRedisRepository repository;
private static final String REDIS_IMAGE = "redis:7.0.8-alpine";
private static final int REDIS_PORT = 6379;
private static final GenericContainer redis;
static {
redis = new GenericContainer(REDIS_IMAGE)
.withExposedPorts(REDIS_PORT);
redis.start();
}
@DynamicPropertySource
private static void registerRedisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port", () -> redis.getMappedPort(REDIS_PORT)
.toString());
}
@Test
void testRedis() {
OAuth2TempInfo savedInfo = repository.save(new OAuth2TempInfo("id", "kakao", "nickname", "email", "url"));
assertThat(savedInfo.getId()).isEqualTo("id");
}
}
또다른 방법
@ContextConfiguration(initializers = RedisContainerInitializer.class)
방식을 사용해도 된다
public class RedisContainerInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext context) {
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("redis:6.0.1"))
.withEnv("TZ", "Asia/Seoul")
.withExposedPorts(6379);
container.start();
Map<String, String> properties = Map.of(
"spring.data.redis.host", container.getHost(),
"spring.data.redis.port", String.valueOf(container.getFirstMappedPort())
);
TestPropertyValues.of(properties).applyTo(context.getEnvironment());
}
}
참고
위에서 적용한 두 방식 모두 ApplicationContextInitializer.initialize
를 통해 PropertySource를 등록한다
class DynamicPropertiesContextCustomizer implements ContextCustomizer {
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
MutablePropertySources sources = context.getEnvironment().getPropertySources();
sources.addFirst(new DynamicValuesPropertySource(PROPERTY_SOURCE_NAME, buildDynamicPropertiesMap()));
}
}
public final class TestPropertyValues {
private void addToSources(MutablePropertySources sources, Type type, String name) {
Map<String, Object> source = new LinkedHashMap<>(this.properties);
sources.addFirst((type.equals(Type.MAP) ? new MapPropertySource(name, source)
: new SystemEnvironmentPropertySource(name, source)));
}
}
(spring boot) RedisRepository 사용하는 방법, @RedisHash
JUnit 5 Quickstart - Testcontainers for Java
Introduction to GitHub Actions
'공부기록 > 기술공유' 카테고리의 다른 글
RequestBody Runtime시 동적 주입 (0) | 2023.11.15 |
---|---|
??? : 어딜 보시는 거죠 그건 제 잔상입니다만?!? (0) | 2023.11.13 |
이력서 저장 테이블 구조 재구성 (0) | 2023.11.10 |
Git Branch Linear (0) | 2023.11.06 |
Git Submodule 적용기 (0) | 2023.10.08 |