DIP 준수 실제 사례

박진형·2025년 1월 18일
0

직접 겪었던 DIP의 필요성을 느꼈던 사례를 기록합니다.

DIP란?

상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.

쉽게 말해서 변경 가능성이 높은 구현체보다는 변경 가능성이 적은 추상체를 의존하라는겁니다.

상황

저는 사내에서 Couchbase를 캐시 솔루션으로 사용하고 있었고, 라이센스가 만료되어 Redis로 전면 전환을 해야하는 상황이었습니다.

그때 상황과 비슷하게 샘플코드를 작성해봤습니다.

@Service
public class WishService {
    private final CouchbaseTemplate couchbaseTemplate;

    public WishService(CouchbaseTemplate couchbaseTemplate) {
        this.couchbaseTemplate = couchbaseTemplate;
    }

    public Long getWishCountByCouchbase(String id) {
        String documentId = "WISHCOUNT:" + id;

        return couchbaseTemplate.findById(Long.class).one(documentId);
    }

}

WishService는 CouchbaseTemplate을 의존하고 있고, 캐시 서버에 저장된 wishcount를 가지고 오는 getWishCountByCouchbase 함수가있습니다.

함수 이름부터 특정 기술을 의존하고있습니다..

여기서 Couchbase를 사용하지 않고 Redis를 사용하고 싶을때는 어떻게 할까요?

아마 다음과 같이 코드를 수정해야할겁니다.

@Service
public class WishService {
    private final RedisTemplate redisTemplate;
    private final ObjectMapper objectMapper;

    public WishService(RedisTemplate redisTemplate, ObjectMapper objectMapper) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }


    public Long getWishCountByRedis(String id) {
        String key = "WISHCOUNT:" + id;

        return objectMapper.convertValue(redisTemplate.opsForValue().get(key), Long.class);
    }

}

겉으로는 간단한 수정이지만 변경 사항이 많고 코드가 복잡할수록 실수할 가능성이 높습니다. 휴먼에러는 무시할 수 없다고 생각됩니다.

couchbase에서 redis로 변경하려고하는데 비즈니스 로직을 건들여야 된다니요, 객체지향, Spring이 추구하는 방향과 맞지않는 것 같습니다.

이렇게 하면 어떨까요?

추상화를 하자

저는 이때 아, DIP를 준수했더라면..! 하는 생각이 들었습니다.
제가 처음부터 작성했던 코드라면 이렇게 작성했었을거야 하면서 말이죠

어떻게 코드를 작성하면 좋을까요? 다음을 살펴봅시다.

public interface CacheRepository {
     <T> T findObject(String key, Class<T> tClass);

    void save(String key, Object value, Duration expiryTime);
}
@Repository
public class CouchbaseCacheRepository implements CacheRepository {
    private final CouchbaseTemplate couchbaseTemplate;

    public CouchbaseCacheRepository(CouchbaseTemplate couchbaseTemplate) {
        this.couchbaseTemplate = couchbaseTemplate;
    }

    @Override
    public <T> T findObject(String key, Class<T> tClass) {
        return couchbaseTemplate.getCouchbaseClientFactory()
                .getCluster()
                .bucket(couchbaseTemplate.getBucketName())
                .defaultCollection()
                .get(key)
                .contentAs(tClass);
    }

    @Override
    public void save(String key, Object value, Duration expiryTime) {
        couchbaseTemplate.getCouchbaseClientFactory()
                .getCluster()
                .bucket(couchbaseTemplate.getBucketName())
                .defaultCollection()
                .upsert(key, value, UpsertOptions.upsertOptions().expiry(expiryTime));
    }
}

CacheRepository라는 인터페이스를 만들고, CouchbaseCacheRepository라는 구현체를 만들었습니다.

그리고 WishService는 CacheRepository 인터페이스를 의존하고 spring의 도움으로 CouchbaseCacheRepository라는 구현체를 주입 시켜줍니다.

@Service
public class WishService {
    private final CacheRepository cacheRepository;

    public WishService(CacheRepository cacheRepository) {
        this.cacheRepository = cacheRepository;
    }


    public Long getWishCount(String id) {
        String key = "WISHCOUNT:" + id;

        return cacheRepository.findObject(key, Long.class);
    }

    public void saveWishCount(String id, Long count) {
        String key = "WISHCOUNT:" + id;

        cacheRepository.save(key, count, Duration.of(60, ChronoUnit.SECONDS));
    }
}

그럼 이제 WishService는 CacheRepository만을 의존하고, 그 구현체가 어떤지는 알 필요 없습니다. 그 구현체가 원하는대로 잘 동작하기만을 응원해주면 됩니다.

이제는 원하는대로 잘 동작하는지 확인해봅니다.
"jinhyung" 이라는 ID에 3이라는 value를 저장해주고 가져와봅니다.


원하는대로 잘 동작합니다.

여기서 couchbase를 사용하다가 redis를 사용하고싶으면 어떻게할까요?

아래와 같이 RedisCacheRepository를 만들어주면 됩니다.

@Repository
public class RedisCacheRepository implements CacheRepository {
    private final RedisTemplate<String, Object> redisTemplate;
    private final ObjectMapper objectMapper;

    public RedisCacheRepository(RedisTemplate<String, Object> redisTemplate, ObjectMapper objectMapper) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }

    @Override
    public <T> T findObject(String key, Class<T> tClass) {
        Object value = redisTemplate.opsForValue()
                .get(key);

        return objectMapper.convertValue(value, tClass);
    }

    @Override
    public void save(String key, Object value, Duration expiryTime) {
        redisTemplate.opsForValue()
                .set(key, value, expiryTime);
    }
}

그리고 기존 CouchbaseCacheRepository는 제거해주거나 @Repository 어노테이션을 제거해서 스프링컨테이너에서 제외시켜주면 되겠습니다.

RedisCacheRepository를 사용하고 다시 결과를 확인해보겠습니다.

기존과 결과는 똑같습니다.

끝내며

이렇게 해서 DIP를 준수함으로써 비즈니스 로직에 손을 대는 위험성없이 Couchbase에서 Redis로 전환해봤습니다.

객체지향이 지향하는 바는 각 객체가 담당하는 역할을 믿고 맡기며 협동하는 것이라고 생각합니다.

적어도 여기선 Couchbase에서 Redis의 변경이 WishService의 책임은 아닐것입니다.

Git Repository

0개의 댓글

관련 채용 정보