그동안 과제를 해오면서 다른 데브코스 일원들이 Facade 레이어를 도입하는 것을 보았었지만, 저는 그동안 필요성을 느끼지 못해서 미뤄왔었는데, 이번 프로젝트에서 처음으로 Facade 레이어를 도입해보았습니다.
뮤지컬을 등록하기 위해서 서비스에서 위와 같이 많은 Repository 에 의존성을 갖게 됩니다. 한 메서드 안에서 각 repository 를 사용하는 로직들을 다 수행해줘야 했기에 너무 많은 책임을 갖고 있고, 그에 따라 유지보수도 어렵고 응집성도 떨어지는 코드가 되었습니다. 테스트를 작성할 때도 하나의 메서드 테스트 안에서 너무 방대한 테스트가 될 것 같다는 생각에 구조를 수정해야한다는 필요성을 느끼게 되었습니다.
이렇게 각 Service 들은 각 Repository 하나의 의존성만 갖도록 해주고 Facade 에서는 그것들을 조합하는 구조로 변경하였습니다. 이렇게 하면 기존에 뮤지컬 서비스 안에 있던 모든 repository 의 로직들을 각각의 서비스가 처리하게 되어 서비스들이 하나의 책임만 갖게 되고 테스트도 훨씬 용이해집니다.
코드가 어떻게 변경되었는지를 한번 보자면,
@Service
@RequiredArgsConstructor
public class MusicalService {
private final MusicalRepository musicalRepository;
private final StadiumRepository stadiumRepository;
private final UserRepository userRepository;
private final SeatGradeRepository seatGradeRepository;
private final ImageUploadService imageUploadService;
private final SeatRepository seatRepository;
private final MusicalSeatRepository musicalSeatRepository;
private final CastingRepository castingRepository;
private final ActorRepository actorRepository;
@Transactional
public MusicalCommandResponseDto create(
MusicalCreateRequestDto createRequestDto,
MultipartFile thumbnail,
List<MultipartFile> detailImages
) throws IOException {
Musical musical = createRequestDto.toEntity();
stadiumRepository.findById(createRequestDto.stadiumId())
.ifPresentOrElse(
stadium -> musical.setStadium(stadium)
, () -> {
throw new EntityNotFoundException("존재하지 않는 공연장입니다");
});
userRepository.findById(createRequestDto.managerId())
.ifPresentOrElse(
user -> musical.setUser(user)
, () -> {
throw new EntityNotFoundException("존재하지 않는 관리자입니다");
});
Musical savedMusical = musicalRepository.save(musical);
createRequestDto.seatGrades()
.forEach(seatGrade -> {
SeatGrade createdSeatGrade = seatGrade.toEntity();
createdSeatGrade.setMusical(musical);
seatGradeRepository.save(createdSeatGrade);
});
createRequestDto.actors()
.forEach(actorId -> {
actorRepository.findById(actorId)
.ifPresentOrElse(
actor -> {
Casting casting = Casting.builder()
.actor(actor)
.musical(musical)
.build();
castingRepository.save(casting);
},
() -> {
throw new EntityNotFoundException("존재하지 않는 배우입니다");
}
);
});
createRequestDto.seats()
.forEach(musicalSeat -> {
Seat seat = seatRepository.findById(musicalSeat.seatId())
.orElseThrow(() -> {
throw new EntityNotFoundException("존재하지 않는 좌석입니다");
});
SeatGrade seatGrade = seatGradeRepository.findSeatGradeByNameAndMusical(musicalSeat.seatGradeName(), musical)
.orElseThrow(() -> {
throw new EntityNotFoundException("존재하지 않는 좌석 등급입니다");
});
MusicalSeat createdMusicalSeat = MusicalSeat.builder()
.seat(seat)
.musical(musical)
.seatGrade(seatGrade)
.build();
musicalSeatRepository.save(createdMusicalSeat);
});
String thumbnailUrl = imageUploadService.uploadImage(thumbnail, musical);
musical.setThumbnailUrl(thumbnailUrl);
imageUploadService.uploadImages(detailImages, musical);
return MusicalCommandResponseDto.from(savedMusical);
}
}
위와 같이 지저분했던 기존의 코드가 아래처럼 변경되었습니다.
@Service
@RequiredArgsConstructor
public class MusicalFacadeService {
private static final String THUMBNAIL_PATH = "musical/thumbnail/";
private static final String DETAIL_IMAGES_PATH = "musical/detailImages/";
private final MusicalService musicalService;
private final StadiumService stadiumService;
private final UserService userService;
private final SeatGradeService seatGradeService;
private final ImageUploadService imageUploadService;
private final MusicalDetailImageService musicalDetailImageService;
private final MusicalSeatService musicalSeatService;
private final CastingService castingService;
private final TicketService ticketService;
private final ScheduleService scheduleService;
private final SeatService seatService;
private final ActorService actorService;
@Transactional
public Long create(
MusicalCreateRequestDTO createRequestDto,
MultipartFile thumbnail,
List<MultipartFile> detailImages
) {
Musical createdMusical = createRequestDto.toEntity();
ImageResponseDTO thumbnailInfo = imageUploadService.uploadImage(thumbnail,THUMBNAIL_PATH);
Stadium stadium = stadiumService.findById(createRequestDto.stadiumId());
User manager = userService.findByIdForFacade(createRequestDto.managerId());
setMusicalAssociation(createdMusical, thumbnailInfo, stadium, manager);
Musical savedMusical = musicalService.save(createdMusical);
List<ImageResponseDTO> detailImagesInfo = imageUploadService.uploadImages(detailImages,DETAIL_IMAGES_PATH);
List<MusicalDetailImage> musicalDetailImages = setMusicalDetailImagesAssociation(detailImagesInfo, savedMusical);
musicalDetailImageService.save(musicalDetailImages);
List<SeatGrade> seatGrades = setSeatGradesAssociation(createRequestDto.seatGrades(), savedMusical);
seatGradeService.save(seatGrades);
List<MusicalSeat> musicalSeats = setMusicalSeatsAssocaition(createRequestDto.seats(), savedMusical);
musicalSeatService.save(musicalSeats);
List<Casting> castings = setCastingsAssociation(createRequestDto.actorIds(), savedMusical);
castingService.save(castings);
return savedMusical.getId();
}
그런데 Facade 레이어를 도입하면서 몇 가지 고민이 생겼습니다.
첫번째로 Controller ↔ Facade ↔ Service ↔ Repository 의 흐름을 가지게 되는데, Service 에서 Facade 로 반환할 때 엔티티를 반환할지 DTO 를 반환할지가 고민이었습니다. 어딘가에서 얼핏 Facade 에서 엔티티 사용은 좋지 않다는 글을 본 것 같았기 때문입니다. 글에서 본 이유가 생각나지 않아 제 나름대로 아래와 같은 이유로 엔티티가 접근해도 상관 없다는 결론을 내렸습니다.
일단, 컨트롤러 단까지 엔티티를 보내지 않는 것에는 대표적으로 아래와 같은 이유가 있습니다.
- 엔티티의 구조를 노출하지 않고 캡슐화
- 즉, 화면에 필요한 정보만 제공함
- 보통 컨트롤러 단은 트랜잭션 범위 밖임
- 엔티티를 컨트롤러까지 접근 허용 시 지연로딩으로 인한 에러 발생 가능성이 있음
- 컨트롤러에서의 엔티티의 변경이 DB 에 반영되지 않음
컨트롤러 단에서 엔티티를 사용하지 않는 주된 이유는 클라이언트에 반환하는 레이어이고 트랜잭션 범위 밖이기 때문입니다. 그렇다면, Facade 는 해당되는 것이 없습니다. Facade 는 클라이언트에 직접적으로 반환해주는 레이어도 아니고, 여러 서비스 로직을 조립해주는 책임을 갖기에 여러 서비스 로직의 생명주기를 같이 가져가야하기 때문에 트랙잭션은 필수입니다. 그렇기에, Facade 에서 엔티티를 사용하지 말아야할 이유가 없다라고 생각합니다.
두번째 고민은 모든 도메인에 Facade 레이어를 도입하였다면 문제가 생기지 않았겠지만, Musical 도메인에서만 Facade 레이어를 도입하여서 생긴 고민입니다.
엔티티를 반환한다면 Musical 도메인에서 사용되는 다른 도메인의 Service 메서드가 Entity 를 반환하기에 Facade 가 없는 다른 도메인에서는 재사용이 불가능하다는 것이 문제가 되고,
그것을 피하고자 DTO 를 반환하면 Facade 층에서 다시 여러 번 변환을 해줘야하기에 불편한 점이 있습니다.
통일감 있는 코드의 중요성을 다시금 느끼게 되었습니다.
'개발' 카테고리의 다른 글
@Modifying 의 flushAutomatically 속성을 명시해야 하는 이유 (0) | 2023.04.09 |
---|---|
스케줄링, 배치로 S3 이미지 삭제 처리 (0) | 2023.04.05 |
AWS S3 에서 이미지를 관리해보자 (0) | 2023.03.27 |
@TransactionalEventListener 파헤치기 (0) | 2023.03.02 |
Let's Encrypt 로 HTTPS 를 적용해보자 (1) | 2023.02.24 |