Framework/Spring

[Spring] 팩토리메서드 패턴 적용해서 확장성 높은 코드 만드는 법

Joonfluence 2024. 3. 7.

문제상황

  • 게시판 목록 조회 API
    • 게시판 유형 별로 다른 테이블을 조회해야 했다.
    • API를 각각 분리하거나, 하나의 게시판 조회 API에서 Type에 따라 if/else 분기에 따라 다른 요소를 조회해야 했다.
      • 예) 일반게시글, 비밀게시글, 인기글A, 인기글B
    • 기존 레거시 시스템에서는 이러한 유형의 데이터들을 if/else로 Service 레이어에서 각각 다른 Repository (여기선 Mapper)를 사용해서 로직을 처리하였다.
@Service
@RequiredArgsConstructor
public class CommunityBoardService {
    private final CommunityContentsFactoryService communityContentsFactoryService;

    @Transactional(readOnly = true)
    public List<CommunityBoardContentsDTO> selectCommunityBoardContents() {
        if (isPersonalBoard) {
            Long optionalUserId = tokenProvider.getUserIdByToken(optionalToken);
            this.validateAccessPersonalContents(optionalUserId, boardSeq);
            results = communityPersonalBoardSlaveMapper.selectPersonalBoardContents(boardSeq, pagingForm);
        } else {
            ContentsDTO contentsDTO = new ContentsDTO(userId, boardSeq, boardCategoryCd, pagingForm);
            results = communitySlaveMapper.selectCommunityBoardContents();
        } ....

        for (CommunityBoardContentsDTO contents : results) {
            contents.setContentsThumbnail();
        }

        return results;
    }
}
  • 이는 Repository 계층과의 결합도가 높은 상태로, 매번 다른 유형의 게시판이 추가될 때마다 Service 계층까지 수정해야 한다는 문제가 발생되었다.

해결방안

팩토리메서드 패턴은 아래와 같은 정의, 장점, 단점, 쓰이는 용례가 존재한다.

  • 정의
    • 팩토리 메서드 패턴은 객체 생성을 처리하기 위한 디자인 패턴 중 하나로, 객체 생성을 서브 클래스에 위임하여 객체를 생성하는 방식으로 동작합한다. 이를 통해, 클라이언트 코드에서는 구체적인 클래스의 인스턴스화를 처리하지 않고, 추상 클래스나 인터페이스를 통해 객체에 접근할 수 있다.
  • 장점
    • 유연성 : 팩토리 메서드 패턴을 사용하면 클라이언트 코드가 구체적인 객체를 인스턴스화하지 않고도 객체에 접근할 수 있다. 이는 클라이언트 코드가 변경되더라도 객체 생성 방식을 변경할 필요가 없도록 만들어준다.
    • 확장성: 새로운 서브 클래스를 추가하여 객체를 생성하는 방식을 확장할 수 있다. 이는 새로운 요구사항이나 변경 사항에 대응하기 쉽게 만들어준다.
    • 코드 중복 최소화: 객체 생성 코드가 클라이언트 코드에 중복되지 않도록 만들어준다. 이는 유지보수성을 향상시키고 코드를 더 간결하게 만든다.
  • 단점
    • 클래스 수 증가: 팩토리 메서드 패턴을 사용하면 추가적인 클래스가 생성되므로 클래스의 수가 늘어날 수 있다. 이는 프로젝트 규모가 커질수록 클래스의 관리가 어려워질 수 있다.
    • 복잡성 증가: 팩토리 메서드 패턴을 사용하면 객체 생성 로직이 서브 클래스로 이동되기 때문에 클래스 간의 상호 의존성이 높아질 수 있다. 이는 코드를 이해하기 어렵게 만들 수 있다.
  • 어느 상황에 쓰면 좋을까
    • 객체 생성 방식이 변할 가능성이 있거나 여러 구현체 중에서 선택해야 하는 경우에 팩토리 메서드 패턴을 사용하는 것이 좋다.
    • 객체 생성 과정이 복잡하거나 객체 간의 관계가 복잡한 경우에도 팩토리 메서드 패턴이 유용할 수 있다.

  • 실제코드
@Service
@RequiredArgsConstructor
public class CommunityContentsFactoryService {
    private final CommunitySlaveMapper communitySlaveMapper;
    private final BoardSlaveMapper boardSlaveMapper;

    public List<CommunityBoardContentsDTO> findContents(ContentsDTO contentsDTO) {
        BoardDTO boardDTO = boardSlaveMapper.selectBoardById(contentsDTO.getBoardSeq());
                // 런타임에서 의존성 주입! 게시판 타입에 따라 다르게 처리
        CommunityContentsQueryService communityContentsQueryService = getCommunityContentsQueryService(boardDTO);
        return communityContentsQueryService.findSelectedContents(contentsDTO);
    }

    private CommunityContentsQueryService getCommunityContentsQueryService(BoardDTO boardDTO) {
        if (BoardSelectedType.TOP_CONTENTS.equals(boardDTO.getType())){
            return new CommunityTopContentsService(communitySlaveMapper);
        } else if (BoardSelectedType.BEST_EXPERIENCE.equals(boardDTO.getType())) {
            return new CommunityBestExperienceService(communitySlaveMapper);
        } else if (BoardSelectedType.PERSONAL_BOARD.equals(boardDTO.getType())) {
            return new CommunityPersonalContentsService(communitySlaveMapper);
        } else {
            return new CommunityContentsService(communitySlaveMapper);
        }
    }
}

이후 (Service 추가해주면 끝)

@Service
@RequiredArgsConstructor
public class CommunityBoardService {
    private final CommunityContentsFactoryService communityContentsFactoryService;

    @Transactional(readOnly = true)
    public List<CommunityBoardContentsDTO> selectCommunityBoardContents() {
        long userId = SecurityUtil.getCurrentUserId();
        List<CommunityBoardContentsDTO> results;
        ContentsDTO contentsDTO = new ContentsDTO();
        results = communityContentsFactoryService.findContents(contentsDTO);
        return results;
    }
}

인터페이스

public interface CommunityContentsQueryService {
    List<CommunityBoardContentsDTO> findSelectedContents();
}

구현체 A) 이런 식으로 무한대로 추가 가능

@RequiredArgsConstructor
public class CommunityBestExperienceService implements CommunityContentsQueryService {
    private final CommunitySlaveMapper communitySlaveMapper;

    @Override
    public List<CommunityBoardContentsDTO> findSelectedContents() {
        return communitySlaveMapper.selectCommunityBoardBestExperienceContents();
    }
}

  • 적용 후 느낀 장점
    • 조회해야 할 게시판 유형이 하나 더 늘어나더라도, 인터페이스의 구현체를 하나 더 추가해주면 됨. 그러면 Service 레이어 코드 수정 없이도, BoardType에 따라 원하는 게시판(테이블)을 조회할 수 있도록 만들어 줄 수 있다. 
    • 보기 싫은 if / else 안봐도 됨! 코드가 아주 깔끔! 

반응형

댓글