Project/땅땅땅

[땅땅땅] RedisTemplate 사용 중, 기술 변경에 유연하고 확장성을 가지도록 리팩토링해보기

S_N_Y 2024. 4. 6. 10:32

 

바꾸게 된 계기 : 문제 인식🐍

현재 프로젝트는 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/

 

DIP : 변경에 유연하고 테스트하기 좋은 코드 설계

tecoble.techcourse.co.kr

https://blog.gangnamunni.com/post/dependency-inversion-principle/

 

외부 툴 변경에 휘둘리지 않는 서버 코드 작성기

사례로 보는, DIP를 이용한 외부 툴에 의존하지 않는 도메인 모델 설계 by 강남언니 블로그

blog.gangnamunni.com

https://techblog.woowahan.com/2561/

 

안정된 의존관계 원칙과 안정된 추상화 원칙에 대하여 | 우아한형제들 기술블로그

{{item.name}} Robert C. Martin의 Agile Software Development – Principles, Patterns, and Practices 에서 SDP, SAP 를 정리해보았습니다. 이 글은 기본적으로는 Java와 Spring Framework 기반(혹은 이와 유사한 계층형 방식)으로

techblog.woowahan.com