빠른 기능 구현위주로 프로젝트를 진행하다 보니, 더럽고 추악한 코드를 만들었습니다.
"어떻게 하면 더 나은 코드로 수정할 수 있을까"에 대해서 알아보다, "디자인 패턴"을 알게 되었습니다.
이번 포스팅에서는 기존 코드의 문제점 및 "전략 패턴"을 이용한 리팩토링 과정을 정리를 하고자 합니다.
아래 코드는 10명의 소환사를 2팀으로 다양한 모드로 섞는 API의 코드입니다.
"다양한 모드"에는 RANDOM, BALANCE, GOLDEN_BALANCE(a.k.a 황금밸런스) 3가지가 존재합니다.
@Slf4j
@RestController
@RequiredArgsConstructor
public class TeamController {
private final TeamService teamService;
@PostMapping("/team")
public SuccessResult<TeamAssignResponseDTO> makeTeamResult(@RequestBody @Validated TeamAssignRequestDTO requestDTO) {
...
TeamAssignResponseDTO result;
if(requestDTO.getAssingMode() == RANDOM) result = teamService.makeResultWithRandomMode(requestDTO);
else if(requestDTO.getAssingMode() == BALANCE) result = teamService.makeResultWithBalanceMode(requestDTO);
else if(requestDTO.getAssingMode() == GOLDEN_BALANCE) result = teamService.makeResultWithGoldenBalanceMode(requestDTO);
return new SuccessResult<>("ok", result);
}
}
새로운 모드가 추가된다면, 컨트롤러 클래스에 새로운 if 조건문을 추가해야합니다
@Service
public class TeamService {
@Autowired
RiotUtils riotUtils;
public TeamAssignResponseDTO makeResultWithRandomMode(TeamAssignRequestDTO requestDTO) {
// 구현
}
public TeamAssignResponseDTO makeResultWithBalanceMode(TeamAssignRequestDTO requestDTO) {
// 구현
}
public TeamAssignResponseDTO makeResultWithGoldenBalanceMode(TeamAssignRequestDTO requestDTO) {
// 구현
}
}
새로운 모드가 추가된다면, 서비스 클래스에 메서드를 추가해야 합니다. 이에 따라 서비스 클래스의 책임이 늘어나고, 코드가 커져서 가독성이 떨어지게 됩니다
SOLID원칙 관점으로, 현재 코드는 다음과 같은 문제점들이 있습니다.
1. 단일 책임 원칙(SRP) 위반
TeamService는 세 가지 다른 모드를 구성하고 있으며, 많은 책임을 맡고 있습니다.
이는 단일 책임 원칙을 위반하며, TeamService 클래스가 커짐에 따라 코드의 가독성과 유지 보수성이 떨어집니다.
2. 의존 역전 원칙(DIP) 위반
TeamController가 TeamService 구현체에 직접 의존하고 있기 때문에, TeamService의 구현이 변경되면 TeamController도 영향을 받게 됩니다.
3. 개방-폐쇄 원칙(OCP) 위반
새로운 팀 구성 방식이 추가될 때마다 TeamService 클래스에 새로운 메서드를 추가해야 합니다.
OCP를 지키기 위해서는, 새로운 팀 구성 방식을 추가할 때 기존 코드를 변경하지 않고 확장할 수 있도록, 인터페이스 또는 추상 클래스를 사용하여 각 팀 구성 방식을 구현하는 방식으로 설계해야 합니다.
- 정책 패턴(Policy Pattern)이라고도 불림
- 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 사용할 수 있게 해줌
- 객체의 행위를 변경하고 싶은 경우 직접 수정하지 않고 전략이라 불리는 캡슐화한 알고리즘을 변경해줌으로써 유연하게 확장하는 방법
실제로 코드에 적용하면서 좀 더 알아보겠습니다.
다음과 같은 구조로 설계할 예정입니다.
@Slf4j
@RestController
@RequiredArgsConstructor
public class TeamController {
private final TeamService teamService;
@PostMapping("/team")
public SuccessResult<TeamAssignResponseDTO> makeTeamResult(@RequestBody @Validated TeamAssignRequestDTO requestDTO) {
TeamAssignResponseDTO result = teamService.executeAssignTeams(requestDTO);
return new SuccessResult<>("ok", result);
}
}
새로운 모드를 추가한다 가정하여도, 수정할 부분이 없습니다.
즉, 클라이언트가 더이상 불필요한 의존성을 갖지 않습니다.
@Service
public class TeamService {
private final Map<TeamAssignMode, TeamAssignmentStrategy> strategies;
@Autowired
public TeamService(List<TeamAssignmentStrategy> strategyList) {
strategies = strategyList.stream().collect(Collectors.toMap(
strategy -> strategy instanceof RandomModeAssignment ? RANDOM :
strategy instanceof BalanceModeAssignment ? BALANCE :
strategy instanceof GoldenBalanceModeAssignment ? GOLDEN_BALANCE : null,
strategy -> strategy));
}
public TeamAssignResponseDTO executeAssignTeams(TeamAssignRequestDTO requestDTO) {
TeamAssignMode mode = requestDTO.getAssingMode();
TeamAssignmentStrategy strategy = strategies.get(mode);
if (strategy == null) throw new IllegalArgumentException("제공하지 않는 모드입니다.");
return strategy.assignTeams(requestDTO);
}
}
새로운 모드를 추가한다 가정하여도, 서비스 코드에 전략만 추가하면 됩니다.
코드 설명
1. List strategyList
- Spring이 TeamAssignmentStrategy 인터페이스를 구현한 모든 클래스를 자동으로 주입합니다.
지금은 RandomModeAssignment 클래스, BalanceModeAssignment 클래스, GoldenBalanceModeAssignment 클래스가 이에 해당합니다.
- Collectors.toMap
- 주입받은 전략 객체 리스트를 Map으로 변환합니다.
- instanceof 키워드를 사용해 Map의 Key값을 정합니다.
public interface TeamAssignmentStrategy {
TeamAssignResponseDTO assignTeams(TeamAssignRequestDTO requestDTO);
}
@Service
public class RandomModeAssignment implements TeamAssignmentStrategy {
...
@Override
public TeamAssignResponseDTO assignTeams() {
//구현
}
@Service
public class BalanceModeAssignment implements TeamAssignmentStrategy {
...
@Override
public TeamAssignResponseDTO assignTeams() {
//구현
}
@Service
public class GoldenBalanceModeAssignment implements TeamAssignmentStrategy {
...
@Override
public TeamAssignResponseDTO assignTeams() {
//구현
}
3개의 모드가 3개의 클래스로 분리되었습니다
단일 책임을 가지도록 분리함으로서, 가독성과 유지 보수성을 챙겼습니다.
또한, 다형성을 이용한 전략 패턴을 적용하여, 구현체에 직접 의존하고 있을떄 생기던 문제들을 해결 하였습니다.
전략 패턴 외에도, 20개가 넘는 디자인 패턴이 존재합니다.
웹 개발에 자주 사용되는 대표적인 디자인 패턴(싱글톤, 빌더, 옵저버)에 대해서 알고있다면, 더 나은 코드에 대해 생각할 떄 도움을 줄 수 있을 것 같습니다.
참고 자료
https://refactoring.guru/ko/design-patterns
https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-OCP-%EA%B0%9C%EB%B0%A9-%ED%8F%90%EC%87%84-%EC%9B%90%EC%B9%99
https://lsj31404.tistory.com/91
https://velog.io/@harinnnnn/OOP-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-5%EB%8C%80-%EC%9B%90%EC%B9%99SOLID-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EB%B6%84%EB%A6%AC-%EC%9B%90%EC%B9%99-ISP
https://kimtaesoo99.tistory.com/216#ISP%20-%20%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%20%EB%B6%84%EB%A6%AC%20%EC%9B%90%EC%B9%99-1