DIP란?
DIP는 SOLID원칙 중 하나로, 고수준 모듈이 저수준 모듈에 의존하는 것이 아니라 "추상화"된 클래스를 의존하도록 만드는 원칙입니다.
스프링에서는 IoC와 DI로 이를 구현할 수 있습니다.

DIP를 적용하게 되면, 일반적인 어플리케이션 아키텍처에서 정의하고 있는 패키지 의존성 흐름대로 바꿀 수 있습니다. 예를 들어,

현재 우리 서비스의 문제점입니다. Redis를 사용하는 수많은 Application계층의 서비스들이 모두 Infrastructure계층을 바라보며, RedisTemplate에 의존하고 있습니다. 이는 고수준 모듈이 저수준 모듈에 의존하게 되는 것이며 DIP를 위반합니다.
이는 유연성을 감소시키고, 테스트를 불편하게 만들며, 모듈 간 결합도가 증가하는 원인이고, 확장성도 떨어지는 설계입니다.
이 때, 추상화된 클래스를 이용하여 패키지 의존성 흐름을 반대로 바꿀 수 있습니다.

인터페이스 KeyValueRepository를 두고, 인터페이스의 구현체가 RedisTemplate으로 이를 구현하게 만들어
Infrastructure계층이 Application계층을 바라보도록 만들 수 있습니다.
해당 개념을 프로젝트에 적용시켜봅시다!
1. RedisTemplate 의존성 리팩토링

‘common/redis’ 안에 RedisTemplate에 의존하는 서비스가 너무 많이 존재합니다.
현재 코드의 문제점: DIP 위반!
RedisTemplate을 직접 호출하다 보니 다른 Key-Value 저장소, 또는 Redisson과 Lettuce로 이전할 때 코드를 전부 바꿔야 합니다.
테스트 코드 작성 시 RedisTemplate Mocking 과정이 상당히 불편합니다.
리팩토링 과정
우선, 추상화합시다! RedisTemplate 호출을 추상화된 인터페이스의 구현체에 맡겨볼까요?
public interface KeyValueRepository {
//Key-Value
String getValue(String key);
void setValue(String key, String number, Duration duration);
void setValue(String key, String value, long ttl, TimeUnit timeUnit);
void removeValue(String key);
}
이제 Redis 결합도가 느슨해지고, Mocking이 훨씬 쉬워졌습니다. 더는 @BeforeEach로 RedisTemplate 행동을 미리 정의하지 않아도 돼요!
@BeforeEach
void setUp() {
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
}
@Test
void incrementDailyLimit_첫번째요청_카운트_1증가_만료시간설정() {
when(valueOperations.get(REDIS_KEY)).thenReturn(null);
assertDoesNotThrow(() -> checkDailyEmailCount.incrementDailyLimit(TEST_EMAIL, OPERATION_KEY));
verify(valueOperations, times(1)).increment(REDIS_KEY);
verify(redisTemplate, times(1)).expire(eq(REDIS_KEY), anyLong(), eq(TimeUnit.SECONDS));
}
(전) @BeforeEach로 valueOperations를 반환해주어야했습니다.
@Test
void incrementDailyLimit_첫번째요청_카운트_1증가_만료시간설정() {
when(keyValueRepository.getValue(REDIS_KEY)).thenReturn(null);
assertDoesNotThrow(() -> checkDailyEmailCount.incrementDailyLimit(TEST_EMAIL, OPERATION_KEY));
verify(keyValueRepository, times(1)).incrementValue(REDIS_KEY);
verify(keyValueRepository, times(1)).expireValue(eq(REDIS_KEY), anyLong(), eq(TimeUnit.SECONDS));
}
(후) 결합도가 낮아져서 테스트코드 작성이 훨씬 쉬워졌어요!
리팩토링 결과
RedisTemplate에서 Redisson과 Lettuce를 사용할 때, Repository의 구현체만 수정하면 돼요!
Redis에서 다른 Key-Value 저장소로 이전하기 쉬워졌어요!
단위 테스트를 수행하기 편해졌어요!
2. KeyValueRepository 책임 과다
public interface KeyValueRepository {
//Key-Value
String getValue(String key);
Set<String> getKeys(String key);
void setValue(String key, String value);
void setValue(String key, String number, Duration duration);
void setValue(String key, String value, long ttl, TimeUnit timeUnit);
void removeValue(String key);
long incrementValue(String key);
void expireValue(String key, long ttl, TimeUnit timeUnit);
Boolean hasKey(String key);
List<String> opsForListRange(String key, long start, long end);
void opsForListLeftPush(String key, String value);
Set<String> opsForZSet(String key, long start, long end);
void opsForZSetAdd(String key, String value, long score);
Long opsForZSetCard(String key);
Set<String> opsForZSetReverseRange(String key, long start, long end);
void opsForZSetRemoveRange(String key, long start, long end);
}
redis를 사용중인 서비스가 생각보다 많았군요..
현재 코드의 문제점: SRP 위반!
하나의 Repository에 너무 많은 메서드가 있고, 이들의 공통점은 Key-value 저장소를 사용한다는 점뿐입니다.
새로운 팀원이 합류한다면, 어떤 메서드를 어디서 찾아서 사용해야 할지 헷갈립니다.
리팩토링 과정
CQRS 아이디어를 차용해 볼까요? 읽기 작업과 쓰기 작업으로 분리합시다!
public interface KeyValueQueryRepository {
//읽기 전용
String getValue(String key);
Set<String> getKeys(String key);
Boolean hasKey(String key);
List<String> getListRange(String key, long start, long end);
Set<String> getZSetRange(String key, long start, long end);
Long getZSetCard(String key);
Set<String> getZSetReverseRange(String key, long start, long end);
}
읽기(Query) 작업을 담는 KeyValueQueryRepository
public interface KeyValueCommandRepository {
//쓰기 전용
void setValue(String key, String value);
void setValue(String key, String value, Duration duration);
void setValue(String key, String value, long ttl, TimeUnit timeUnit);
void removeValue(String key);
long incrementValue(String key);
void expireValue(String key, long ttl, TimeUnit timeUnit);
void pushToListLeft(String key, String value);
void addToZSet(String key, String value, long score);
void removeFromZSetRange(String key, long start, long end);
}
쓰기(Command) 작업을 담는 KeyValueCommandRepository
리팩토링 결과
로직을 나누어 협업하기 쉬워졌어요!
유지 보수하기 편해졌어요!
패턴이 명확하여 다른 Key-Value 저장소로 이전하기 쉬워졌어요!
'팀 프로젝트 > cheerha.project' 카테고리의 다른 글
사용자가 엔드포인트를 잘못 입력했을 때, 404를 반환시키자! (0) | 2025.03.15 |
---|---|
ALB에 Web Application Firewall 적용하기 (0) | 2025.03.13 |
Grafana Alert Rule 설정하기 with Slack (0) | 2025.03.12 |