Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
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]20241120 FACADE 패턴 본문

개발Article

[TIL]20241120 FACADE 패턴

최밤빵 2024. 11. 20. 04:05

FACADE 패턴 시스템의 복잡한 부분을 감추고, 단순화된 인터페이스를 제공함으로써 사용자가 쉽게 사용할 수 있도록 돕는 디자인 패턴이다. 이번 개발일지를 통해 FACADE 패턴의 개념을 학습하고 이를 실제 코드에 어떻게 적용할 수 있는지 이해해보려고 한다.

 

▶FACADE 패턴?

FACADE 패턴은 복잡한 서브 시스템이나 여러 클래스들로 구성된 코드를 단순화하기 위해 사용하는 디자인 패턴이다. 이 패턴은 여러 개의 클래스들이 서로 복잡하게 상호작용하는 시스템을 단순하게 사용할 수 있는 단일 인터페이스를 제공한다. 복잡한 내부 구조를 숨기고 외부에서는 단순한 메서드 호출만으로도 다양한 기능을 사용할 수 있게 만들어 준다.

이 패턴의 목적은 코드의 복잡성을 줄이고 유지보수성을 높이는 것이다. 개발자가 직접 여러 클래스와 상호작용하기보다, FACADE를 통해 단순한 인터페이스로 상호작용하게 하여 코드의 가독성사용성을 높인다.


▶예시 코드 분석

내가 작성한 GamingSetupService 클래스의 예시 코드로, 이 코드는 여러 DTO와 엔티티, 레포지토리 등을 사용해 게임 설정을 관리하는 서비스이다. 현재 이 코드는 서비스 내부에서 다양한 데이터 변환과 검증 로직을 직접 처리하고 있어 코드가 조금 복잡하고, 유지보수가 어렵게 느껴진다. ( 정말 굴러가게만 만들어놨더니........😵‍💫)

package com.sample.pugbrecords.gamingSetup.service;

import com.sample.pugbrecords.gamingSetup.dto.GamingSetupRequestDto;
import com.sample.pugbrecords.gamingSetup.dto.GamingSetupResponseDto;
import com.sample.pugbrecords.gamingSetup.entity.GamingSetup;
import com.sample.pugbrecords.gamingSetup.repository.GamingSetupRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
public class GamingSetupService {

    private final GamingSetupRepository gamingSetupRepository;

    public GamingSetupService(GamingSetupRepository gamingSetupRepository) {
        this.gamingSetupRepository = gamingSetupRepository;
    }

    public List<GamingSetupResponseDto> getAllGamingSetups() {
        return gamingSetupRepository.findAll().stream()
                .map(this::convertToResponseDto)
                .collect(Collectors.toList());
    }

    public Optional<GamingSetupResponseDto> getGamingSetupByPlayer(String player) {
        return gamingSetupRepository.findByPlayer(player).map(this::convertToResponseDto);
    }

    public GamingSetupResponseDto createGamingSetup(GamingSetupRequestDto gamingSetupRequestDto) {
        GamingSetup gamingSetup = convertToEntity(gamingSetupRequestDto);
        GamingSetup savedSetup = gamingSetupRepository.save(gamingSetup);
        return convertToResponseDto(savedSetup);
    }

    public Optional<GamingSetupResponseDto> updateGamingSetup(String player, GamingSetupRequestDto gamingSetupRequestDto) {
        Optional<GamingSetup> existingSetup = gamingSetupRepository.findByPlayer(player);
        if (existingSetup.isPresent()) {
            GamingSetup gamingSetup = existingSetup.get();
            updateEntityFromDto(gamingSetupRequestDto, gamingSetup);
            GamingSetup updatedSetup = gamingSetupRepository.save(gamingSetup);
            return Optional.of(convertToResponseDto(updatedSetup));
        }
        return Optional.empty();
    }

    public Optional<GamingSetupResponseDto> partialUpdateGamingSetup(String player, GamingSetupRequestDto gamingSetupRequestDto) {
        Optional<GamingSetup> existingSetup = gamingSetupRepository.findByPlayer(player);
        if (existingSetup.isPresent()) {
            GamingSetup gamingSetup = existingSetup.get();
            updateEntityFromDto(gamingSetupRequestDto, gamingSetup);
            GamingSetup updatedSetup = gamingSetupRepository.save(gamingSetup);
            return Optional.of(convertToResponseDto(updatedSetup));
        }
        return Optional.empty();
    }

    public boolean deleteGamingSetup(String player) {
        Optional<GamingSetup> existingSetup = gamingSetupRepository.findByPlayer(player);
        if (existingSetup.isPresent()) {
            gamingSetupRepository.delete(existingSetup.get());
            return true;
        }
        return false;
    }

    private GamingSetup convertToEntity(GamingSetupRequestDto dto) {
        return new GamingSetup(
                dto.getPlatform(),
                dto.getPlayer(),
                dto.getDpi(),
                dto.getGeneralSens(),
                dto.getVerticalSens(),
                dto.getAimSens(),
                dto.getAimSens1x(),
                dto.getAimSens2x(),
                dto.getAimSens3x(),
                dto.getAimSens4x(),
                dto.getAimSens6x(),
                dto.getAimSens8x(),
                dto.getAimSens15x(),
                dto.getResolution(),
                dto.getFov(),
                dto.getMonitor(),
                dto.getMouse(),
                dto.getMousepad(),
                dto.getKeyboard(),
                dto.getHeadset(),
                dto.getSoundCard(),
                dto.getMonitorLink(),
                dto.getMouseLink(),
                dto.getMousepadLink(),
                dto.getKeyboardLink(),
                dto.getHeadsetLink()
        );
    }

    private GamingSetupResponseDto convertToResponseDto(GamingSetup entity) {
        return new GamingSetupResponseDto(
                entity.getId(),
                entity.getPlatform(),
                entity.getPlayer(),
                entity.getDpi(),
                entity.getGeneralSens(),
                entity.getVerticalSens(),
                entity.getAimSens(),
                entity.getAimSens1x(),
                entity.getAimSens2x(),
                entity.getAimSens3x(),
                entity.getAimSens4x(),
                entity.getAimSens6x(),
                entity.getAimSens8x(),
                entity.getAimSens15x(),
                entity.getResolution(),
                entity.getFov(),
                entity.getMonitor(),
                entity.getMouse(),
                entity.getMousepad(),
                entity.getKeyboard(),
                entity.getHeadset(),
                entity.getSoundCard(),
                entity.getMonitorLink(),
                entity.getMouseLink(),
                entity.getMousepadLink(),
                entity.getKeyboardLink(),
                entity.getHeadsetLink()
        );
    }

    private void updateEntityFromDto(GamingSetupRequestDto dto, GamingSetup entity) {
        if (dto.getPlatform() != null && !dto.getPlatform().isEmpty()) entity.setPlatform(dto.getPlatform());
        if (dto.getPlayer() != null && !dto.getPlayer().isEmpty()) entity.setPlayer(dto.getPlayer());
        if (dto.getDpi() != 0) entity.setDpi(dto.getDpi());
        if (dto.getGeneralSens() != 0) entity.setGeneralSens(dto.getGeneralSens());
        if (dto.getVerticalSens() != 0) entity.setVerticalSens(dto.getVerticalSens());
        if (dto.getAimSens() != 0) entity.setAimSens(dto.getAimSens());
        if (dto.getAimSens1x() != 0) entity.setAimSens1x(dto.getAimSens1x());
        if (dto.getAimSens2x() != 0) entity.setAimSens2x(dto.getAimSens2x());
        if (dto.getAimSens3x() != 0) entity.setAimSens3x(dto.getAimSens3x());
        if (dto.getAimSens4x() != 0) entity.setAimSens4x(dto.getAimSens4x());
        if (dto.getAimSens6x() != 0) entity.setAimSens6x(dto.getAimSens6x());
        if (dto.getAimSens8x() != 0) entity.setAimSens8x(dto.getAimSens8x());
        if (dto.getAimSens15x() != 0) entity.setAimSens15x(dto.getAimSens15x());
        if (dto.getResolution() != null && !dto.getResolution().isEmpty()) entity.setResolution(dto.getResolution());
        if (dto.getFov() != 0) entity.setFov(dto.getFov());
        if (dto.getMonitor() != null && !dto.getMonitor().isEmpty()) entity.setMonitor(dto.getMonitor());
        if (dto.getMouse() != null && !dto.getMouse().isEmpty()) entity.setMouse(dto.getMouse());
        if (dto.getMousepad() != null && !dto.getMousepad().isEmpty()) entity.setMousepad(dto.getMousepad());
        if (dto.getKeyboard() != null && !dto.getKeyboard().isEmpty()) entity.setKeyboard(dto.getKeyboard());
        if (dto.getHeadset() != null && !dto.getHeadset().isEmpty()) entity.setHeadset(dto.getHeadset());
        if (dto.getSoundCard() != null && !dto.getSoundCard().isEmpty()) entity.setSoundCard(dto.getSoundCard());
    }
}

▶FACADE 패턴 적용해보기

위 코드는 다양한 데이터 변환과 검증, 데이터베이스 접근이 한 클래스에 집중되어 있어 코드가 복잡하고, 수정할 때 많은 부분을 건드려야 할 가능성이 있다. 이러한 복잡성을 줄이기 위해 FACADE 패턴을 적용해볼 수 있다.

 

▶FACADE 클래스 추가

FACADE 패턴을 적용하기 위해 GamingSetupFacade라는 클래스를 추가해 본다. 이 클래스는 여러 복잡한 작업들을 간단한 메서드로 제공하는 역할을 하게 된다.

public class GamingSetupFacade {

    private final GamingSetupService gamingSetupService;

    public GamingSetupFacade(GamingSetupService gamingSetupService) {
        this.gamingSetupService = gamingSetupService;
    }

    public GamingSetupResponseDto createSetupWithValidation(GamingSetupRequestDto requestDto) {
        // 이곳에서 입력 데이터에 대한 추가적인 검증 로직을 수행할 수 있다.
        if (requestDto.getPlayer() == null || requestDto.getPlayer().isEmpty()) {
            throw new IllegalArgumentException("플레이어 이름은 필수입니다.");
        }
        return gamingSetupService.createGamingSetup(requestDto);
    }

    public List<GamingSetupResponseDto> getAllSetups() {
        return gamingSetupService.getAllGamingSetups();
    }

    public Optional<GamingSetupResponseDto> getSetupByPlayer(String player) {
        return gamingSetupService.getGamingSetupByPlayer(player);
    }
}

→ 이 FACADE 클래스는 기존의 GamingSetupService에서 제공하는 다양한 메서드를 단순화된 인터페이스로 제공하여, 사용자가 쉽게 게임 설정을 생성하거나 조회할 수 있게 해준다. 이렇게 FACADE 클래스를 추가함으로써, 서비스 클래스의 복잡한 내부 로직을 감추고 간단한 메서드로 외부에 노출함으로써 코드의 가독성을 높이고, 사용자 코드에서의 복잡성을 줄일 수 있다.


▶FACADE 패턴의 장점과 단점

  • 장점
  1. 복잡성 감소: 여러 클래스를 한 번에 다루는 복잡한 로직을 FACADE 클래스를 통해 단순화할 수 있다.
  2. 유지보수 용이: FACADE를 통해 기능을 노출함으로써, 내부 로직을 수정해도 외부에서는 단순한 인터페이스만 유지하면 되기 때문에 유지보수가 용이하다.
  3. 코드 가독성 향상: 복잡한 내부 로직을 감춤으로써, 클라이언트 코드가 훨씬 깔끔해진다.
  • 단점
  1. 추상화의 위험성: 내부 로직이 FACADE로 감춰지면서, FACADE 내부에서 일어나는 세부적인 동작을 파악하기 어려울 수 있다.
  2. 오버헤드: 단순히 단순화를 위해 FACADE를 추가하면, 코드의 계층이 늘어나 오히려 불필요한 복잡도가 추가될 수 있다.

FACADE 패턴은 복잡한 시스템을 간단한 인터페이스로 감싸서 사용자에게 제공하는 패턴으로복잡한 시스템을 간단하게 사용할 수 있게 해주는 유용한 디자인 패턴임을 알게 되었다. 이번 예시에서는 복잡한 게임 설정 서비스 로직을 FACADE를 통해 감싸서, 사용자가 더 쉽게 접근할 수 있도록 만들었다. 이를 통해 코드의 복잡성을 줄이고 유지보수성을 높이는 것이 가능했다. FACADE 패턴을 사용해보면서 느낀 점은, 이 패턴이 코드의 복잡한 부분을 숨겨서 더 단순하게 만들 수 있다는 점이다. 그러나 추상화가 너무 많아지면 내부 동작을 이해하기 어렵게 될 수 있다는 단점도 명확히 인식할 수 있었다.