바꾸게 된 계기 : 문제 인식🐍
현재 프로젝트는 Redis를 이용해서
분산락과 캐싱, Keyspace Notification, SSE를 위한 Pub/Sub까지 최소 4가지 이상의 기술들을 사용하고 있는데
...
이 중에서 하나라도 Redis가 아닌 다른 기술을 사용하게 된다면?🥵
코드를 둘러보던 도중 하나의 기술을 의존하고 있어서 차마 생각을 못 했었는데 좀 더 근본적인 문제는 따로 있었음을 인지했고 그에 따라 기술에 의존적인 코드를 작성하고 있었음을 인지하게 되었다. 그래서 리팩토링을 통해서 기술 의존적인 코드를 개선한 기록을 남기고자 한다.
구현체에 의존하는 코드를 같이 알아보자🔍
@Service
@RequiredArgsConstructor
public class BidEventPublisher {
private final RedisTemplate<String, String> redisTemplate;
private final static String EVENT_TOPIC = "auction-price:";
public void publishEvent(Long auctionId, String event) {
redisTemplate.convertAndSend(EVENT_TOPIC + auctionId, event);
}
}
실시간 입찰가를 반영하기 위한 Redis Pub/Sub + SSE를 구현하는 과정에서 만들어진 메서드이다.
이렇게 Redis를 spring에서 이용하기 위해 Bean으로 등록된 RedisTemplate을 그대로 필드에 주입 받아서 사용하고 있었다.
그렇다면 해당 코드의 문제점은 무엇일까?🧐
=> 비즈니스 로직이 RedisTemplate이라는 구체적인 구현체와 강하게 결합되고 있다는 점이다.
앞서 이야기 했다시피 Redis가 아니라 Kafka나 RabbitMQ로 마이그레이션을 한다면 이에 따라 도메인 내 코드를 수정해야 한다. 지금의 코드와 같이 만약 RedisTemplate를 쓰는 곳이 100곳이라면 100곳의 코드를 모두 수정해야하는 대참사가 발생할 것이다.
즉, 객체 지향 5대 원칙 (SOLID) 중 OCP(Open-Closed Principle : 개방/폐쇄 원칙) 확장에는 열려있고 수정에는 닫혀 있는 코드 (Redis -> Kafka의 경우 수정이 아주 많이 일어나야 한다)와 DIP(Dependency Inversion Principle : 인터페이스 분리 원칙) 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안되는, 즉 인터페이스를 각각 사용에 맞게끔 잘게 분리해야 하는데 고수준의 Service 모듈이 구체적인 RedisTemplate에 의존하고 있는 것이다.
=> 결론적으로 원칙 2개를 위배하고 있는 적절치 않은 코드이다.
기술 의존적인 메서드를 추상화해보자🍀
로직을 수행하는 메서드를 다시 살펴보고 추상화할 수 있는 부분을 짚어보자
아래와 같이 인터페이스화 시키고 해당 인터페이스(PubSubService)의 구현체에서 구체적인 로직을 작성하면 된다.
public interface PubSubService {
void setAuctionExpiredKey(Long auctionId);
void publishEvent(String eventTopic, Long auctionId, String event);
}
@Slf4j(topic = "RedisService")
@RequiredArgsConstructor
@Component
public class RedisService implements PubSubService {
private final RedisTemplate<String, String> redisTemplate;
@Override
public void setAuctionExpiredKey(Long auctionId) {
ValueOperations<String, String> operations = redisTemplate.opsForValue();
String redisKey = "auctionId:" + auctionId;
operations.set(redisKey, "1");
redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES);
log.info("경매 등록, " + redisKey);
}
@Override
public void publishEvent(String eventTopic, Long auctionId, String event) {
redisTemplate.convertAndSend(eventTopic + auctionId, event);
}
}
결과 : 이제 서비스는 RedisTemplate이라는 구체적인 구현체를 모르게 된다.
...
이 리팩토링이 가지는 장점은 무엇일까?🤸
Service는 어떻게 실행되는지 구체적으로 알 필요가 없어진다.
즉, 다른 구현체가 와도 리턴 값만 확인해주면 되기 때문에 Redis가 Kafka가 되든, 캐싱처리의 경우 Memcached가 되든 Service의 코드에 변경점이 생기지 않는다.
+) 인터페이스가 만능은 아니다⚠️
무분별한 추상화는 복잡한 코드와 다량의 클래스를 낳을 수 있기 때문이다.
그래서 무조건 인터페이스화하기보다 외부 서비스를 사용(AWS, GCP..)할 때 인터페이스화하면 유연하게 사용할 수 있을 것이다.
<References>
https://tecoble.techcourse.co.kr/post/2021-11-21-dip/
https://blog.gangnamunni.com/post/dependency-inversion-principle/
'Project > 땅땅땅' 카테고리의 다른 글
Redis Keyspace Notifications란? 그리고 spring boot에 적용하는 방법 (0) | 2024.04.14 |
---|---|
[땅땅땅] ObjectMapper 빼고 코드 최적화해보기 - 리팩토링 (0) | 2024.03.30 |