Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[TIL]20241212 모든 샤드 클래스의 갱신 요청 처리 설계 본문

개발Article

[TIL]20241212 모든 샤드 클래스의 갱신 요청 처리 설계

최밤빵 2024. 12. 12. 01:21

이전 개발일지에서 Redis를 활용한 캐싱과 갱신 요청 처리 방안을 고민했다. 이번에는 모든 샤드 클래스에 대해 갱신 요청 기능을 추가해야 하는 상황의 다른방법을 찾기 위해 효율적인 설계 방안을 찾아야했다. 모든 샤드 클래스를 의존성 주입 방식으로 관리하는 방법도 있지만, 비효율적이고 유지보수에 불리하기 때문에, 여러 설계 방안을 비교하고, 가장 적합한 방안을 선정했다. 


1. 문제 

기존 방식의 한계
  • 의존성 주입 방식
    • 모든 샤드 클래스를 컨트롤러나 서비스 레이어에 직접 의존성 주입하면, 클래스가 많아질수록 복잡도가 증가한다.
    • 새로운 샤드 클래스가 추가될 때마다 기존 컨트롤러나 서비스 로직을 수정해야 하니까 확장성이 낮다.
요구사항
  • 각 샤드 클래스는 고유의 갱신 로직을 유지해야 한다.
  • 갱신 요청 처리는 공통된 흐름으로 관리하되, 클래스별로 분리된 구현을 지원해야 한다.
  • 유지보수성과 확장성을 확보해야 한다.

2. 대안 설계

공통 인터페이스 + 매니저 기반 설계
  • 모든 샤드 클래스에 공통 인터페이스를 도입하여 갱신 로직을 통합 관리.
  • 매니저 클래스를 통해 요청을 동적으로 라우팅.
동적 서비스 탐색 
  • Spring의 ApplicationContext를 사용하여 요청 시점에 필요한 샤드 서비스를 탐색.
  • 컴포넌트를 동적으로 관리하므로, 코드 수정 없이 새로운 샤드 클래스를 추가 가능.
ENUM 기반 라우팅
  • 샤드 클래스를 ENUM으로 정의하고, ENUM과 샤드 클래스 간 매핑을 통해 요청 처리.
  • 간단한 구현과 높은 가독성을 제공하나, 샤드 클래스가 많아지면 ENUM 관리가 복잡해질 수 있다.

4. 가장 적합한 방안: Spring ApplicationContext 활용

선정 이유
  • Spring의 DI 컨테이너를 활용하여 동적으로 서비스를 탐색하므로 유지보수성과 확장성이 뛰어나다.
  • 모든 샤드 클래스가 동일한 인터페이스를 구현하고, 런타임에 필요한 구현체를 동적으로 조회할 수 있다.
동적 서비스 탐색의 장점

- 확장성과 유지보수성
새로운 샤드 클래스가 추가될 경우, @Service로 등록하고 Updatable 인터페이스를 구현하기만 하면 된다.
기존 컨트롤러나 매니저 로직을 수정하지 않아도 자동으로 새로운 클래스를 처리할 수 있다.
코드 변경이 최소화되기때문에 유지보수성이 높아진다.

- 책임 분리
각 샤드 클래스는 자신의 고유 로직만 구현하기때문에, 코드가 깔끔하고 단순하다.
각 레이어의 역할이 명확해져 가독성과 유지보수성이 동시에 확보된다.

- 코드 재사용성 및 중복 제거
매니저 클래스에서 모든 샤드 클래스를 통합 관리하기때문에, 컨트롤러에서 샤드별로 반복되는 호출 코드를 작성할 필요가 없다.
공통적인 처리 로직(서비스 탐색이나 오류 처리 등)을 매니저에서 일괄적으로 관리할 수 있다.

- 동적 서비스 관리
Spring의 ApplicationContext를 활용해 런타임에 필요한 서비스를 동적으로 탐색하기때문에, 코드가 유연해진다. 
요청 타입(ex. match, player)에 따라 적합한 서비스를 자동으로 라우팅한다.

5. 구현 예시

 

1) Updatable 인터페이스

모든 샤드 클래스는 이 인터페이스를 구현하고 공통 구조를 따른다.

public interface Updatable<T> {
    T update(String id);
}

2) 샤드 클래스 구현

MatchShardService 예시 

@Service
public class MatchShardService implements Updatable<MatchData> {

    private final RedisTemplate<String, MatchData> redisTemplate;
    private final ExternalApiService externalApiService;

    public MatchShardService(RedisTemplate<String, MatchData> redisTemplate, ExternalApiService externalApiService) {
        this.redisTemplate = redisTemplate;
        this.externalApiService = externalApiService;
    }

    @Override
    public MatchData update(String matchId) {
        String cacheKey = "PUBG:match:" + matchId;

        // API 호출 후 Redis 갱신
        MatchData latestData = externalApiService.fetchFromApi(matchId);
        redisTemplate.opsForValue().set(cacheKey, latestData, Duration.ofHours(1));

        return latestData;
    }
}

3) ApplicationContext 활용한 서비스 탐색

Spring의 ApplicationContext를 통해 필요한 구현체를 동적으로 조회한다. 

@Component
public class ShardServiceManager {

    private final ApplicationContext applicationContext;

    public ShardServiceManager(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public <T> Updatable<T> getShardService(String type) {
        String beanName = type + "matchShardService";
        try {
            return (Updatable<T>) applicationContext.getBean(beanName);
        } catch (BeansException e) {
            throw new IllegalArgumentException("해당 타입의 샤드 서비스가 없습니다: " + type, e);
        }
    }
}

4) 컨트롤러 구현

컨트롤러는 요청을 ShardServiceManager로 전달하여 필요한 서비스를 호출한다. 

@RestController
@RequestMapping("/update")
public class UpdateController {

    private final ShardServiceManager shardServiceManager;

    public UpdateController(ShardServiceManager shardServiceManager) {
        this.shardServiceManager = shardServiceManager;
    }

    @PostMapping("/{type}")
    public ResponseEntity<?> update(@PathVariable String type, @RequestParam String id) {
        try {
            Updatable<?> service = shardServiceManager.getShardService(type);
            Object updatedData = service.update(id);
            return ResponseEntity.ok(updatedData);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body("갱신 실패: " + e.getMessage());
        }
    }
}

Spring의 ApplicationContext를 활용한 동적 서비스 탐색 방식은 여러 이점을 제공한다.
  1. 확장성: 새로운 샤드 클래스 추가 시, @Service와 인터페이스 구현만으로 자동으로 처리 가능.
  2. 유지보수성: 기존 컨트롤러나 서비스 로직을 수정하지 않아도 되므로 코드 변경이 최소화된다.
  3. 책임 분리: 컨트롤러는 요청 전달만 담당하고, 비즈니스 로직은 서비스와 매니저에 집중된다.
  4. 코드 단순화: 컨트롤러와 서비스 레이어의 복잡성을 줄이고, 동적으로 샤드 클래스를 관리.

이번 설계를 통해 모든 샤드 클래스의 갱신 요청을 효율적으로 처리할 수 있는 구조를 찾아내긴 했다. 앞으로 샤드 클래스가 추가되거나 변경되더라도 유지보수가 괜찮게 이루어질 것 같다..!