Project/Afternote

[트러블슈팅] OCP 위반 코드 수정

gyuun365 2026. 3. 22. 19:30

확장에 열려 있고 수정에 닫혀 있는 구조를 설계할 때 가장 핵심이 되는 개념은 개방-폐쇄 원칙(Open-Closed Principle, OCP)이다.

하지만 프로젝트 마무리 때 긴급하게 하느라 이것을 위반하고 그저 하드 코딩을 했었다.

 

 public void saveRelationsByCategory(Afternote afternote, AfternoteCreateRequest request) {
        // 모든 카테고리에서 receiver 저장 (옵션)
        if (request.getReceivers() != null && !request.getReceivers().isEmpty()) {
            saveReceivers(afternote, request);
        }
        
        switch (request.getCategory()) {
            case SOCIAL:
                saveSocialCredentials(afternote, request);
                break;
            case GALLERY:
                // receivers는 위에서 이미 처리됨
                break;
            case PLAYLIST:
                savePlaylist(afternote, request);
                break;
        }
    }

나름대로의 확장성을 추구했지만 저 밑의 헬퍼함수들이 늘어가면서 코드들은 계속해서 길어져 갔고 카테고리가 생긴다면 훨씬 더 복잡해질 것 같았다. 또한 비슷한 코드가 updateRelationByCategory에도 존재했기 때문에 다른 전략을 구사해야할 것 같았다.

Strategy + Factory pattern 적용

그래서 나는 strategy 와 Factory pattern을 적용하고자 마음 먹었다. 카테고리가 아무리 추가 되더라도 나는 그에 맞는 Strategy를 추가해주기만 하면 기존 코드들을 수정할 필요가 없는 것이다.

package com.afternote.domain.afternote.service.relation;

import com.afternote.domain.afternote.dto.AfternoteCreateRequest;
import com.afternote.domain.afternote.model.Afternote;
import com.afternote.domain.afternote.model.AfternoteCategoryType;

public interface AfternoteCategoryRelationStrategy {

    AfternoteCategoryType category();

    void save(Afternote afternote, AfternoteCreateRequest request);

    void update(Afternote afternote, AfternoteCreateRequest request);
}

그래서 일단 interface를 만들어 save와 update 전략을 각각의 factory 안에 넣어놓기로 했다.

package com.afternote.domain.afternote.service.relation;

import com.afternote.domain.afternote.model.AfternoteCategoryType;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.EnumMap;
import java.util.List;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class AfternoteRelationStrategyFactory {

    private final List<AfternoteCategoryRelationStrategy> strategies;
    private final Map<AfternoteCategoryType, AfternoteCategoryRelationStrategy> strategyMap =
            new EnumMap<>(AfternoteCategoryType.class);

    @PostConstruct
    void init() {
        for (AfternoteCategoryRelationStrategy strategy : strategies) {
            if (strategyMap.containsKey(strategy.category())) {
                throw new IllegalStateException("Duplicate relation strategy for category: " + strategy.category());
            }
            strategyMap.put(strategy.category(), strategy);
        }
    }

    public AfternoteCategoryRelationStrategy get(AfternoteCategoryType categoryType) {
        AfternoteCategoryRelationStrategy strategy = strategyMap.get(categoryType);
        if (strategy == null) {
            throw new IllegalStateException("No relation strategy for category: " + categoryType);
        }
        return strategy;
    }
}

그래서 이런식으로 만들어버리면 Map 형태로 strategy가 저장되어서 런타임 중에도 바꿀 수도 있다는 유연성을 얻을 수 있었고, 코드 중복들을 없앨 수가 있었다. 

public void saveRelationsByCategory(Afternote afternote, AfternoteCreateRequest request) {
        // 모든 카테고리에서 receiver 저장 (옵션)
        if (request.getReceivers() != null && !request.getReceivers().isEmpty()) {
            saveReceivers(afternote, request);
        }

        AfternoteCategoryRelationStrategy strategy = relationStrategyFactory.get(request.getCategory());
        strategy.save(afternote, request);
    }

그래서 실제 활용할 때 strategy를 category를 따라 불러와서 저장되어 있는 save 혹은 update를 사용할 수가 있는 것이다. 

 

validation도 마찬가지로 적용 완료 했다.