조급하면 모래성이 될뿐

Service에서 다른 Service를 의존하게 된다면 ? 본문

TroubleShooting/데브코스

Service에서 다른 Service를 의존하게 된다면 ?

Pawer0223 2022. 7. 6. 20:11

문제 상황


public class ReservedSeatService {

    private final ReservedSeatRepository reservedSeatRepository;
    private final SeatConverter seatConverter;
    private final SeatService seatService;
    private final ScheduleService scheduleService;

    public ReservedSeatService(ReservedSeatRepository reservedSeatRepository, SeatConverter seatConverter,
        SeatService seatService, ScheduleService scheduleService) {
        this.reservedSeatRepository = reservedSeatRepository;
        this.seatConverter = seatConverter;
        this.seatService = seatService;
        this.scheduleService = scheduleService;
    }
    
    public List<ResponseFindSeat> findReservePossibleSeats(Long scheduleId) {
        Schedule schedule = scheduleService.findById(scheduleId);
        List<Long> reservedSeatIdList = reservedSeatRepository.searchByScheduleIdStartsWith(scheduleId).stream()
            .map((reservedSeat) -> reservedSeat.getSeat().getId())
            .collect(toList());

        return seatService.findRemainSeats(schedule.getTheaterRoom().getId(), reservedSeatIdList).stream()
            .map(seatConverter::convertFromSeatToResponseFindSeat)
            .collect(toList());
    }

}
  • ReservedSeatService에서 다른 도메인의 Service를 사용했다.

 

뭐가 문제?


  • ReservedSeatService의 책임이 불분명하다.
    • ReservedSeatService가 ReservedSeatRepository와 1:1 매핑이 되는 Service로 사용되지 않는다.
      • DAO와 1:1 매핑을 맺는 Service가 필수는 아니다. 하지만 나는 재사용 측면에서 이런 Service가 필요하다 생각했다.
      • 처음에는 이러한 기준 없이 구현을 했다.
    • 따라서 findReservePossibleSeats 메서드의 역할이 너무 많아졌다.
      • 현재 예매된 좌석 리스트를 찾은 후, 예매 가능한 좌석 정보를 반환한다.

 

첫 번째 시도 - 의존성을 컨트롤러에서 받아서 처리하기


먼저 findReservePossibleSeats함수를 예약 좌석 리스트를 찾는 역할만을 수행하도록 변경했다.

 

@Service
@Transactional(readOnly = true)
public class ReservedSeatService {
	private final ReservedSeatRepository reservedSeatRepository;
	private final SeatConverter seatConverter;
    
	public ReservedSeatService(ReservedSeatRepository reservedSeatRepository, SeatConverter seatConverter) {
		this.reservedSeatRepository = reservedSeatRepository;
		this.seatConverter = seatConverter;
	}

	public List<Long> findReservedIdListByScheduleId(Long scheduleId) {
		return reservedSeatRepository.searchByScheduleIdStartsWith(scheduleId).stream()
			.map((reservedSeat) -> reservedSeat.getSeat().getId())
			.collect(toList());
	}
    
    ...
    
}

그리고 기존 ReservedSeatService에 처리하던 다른 Service 호출을 Controller로 변경했다.

public class ReservedSeatController {

	private final ReservedSeatService reservedSeatService;
	private final SeatService seatService;
	private final ScheduleService scheduleService;

	public ReservedSeatController(ReservedSeatService reservedSeatService,
		SeatService seatService, ScheduleService scheduleService) {
		this.reservedSeatService = reservedSeatService;
		this.seatService = seatService;
		this.scheduleService = scheduleService;
	}

	@GetMapping("/{scheduleId}/possible")
	public ResponseEntity<List<ResponseFindSeat>> findReservePossibleSeats(@PathVariable Long scheduleId) {
		Schedule schedule = scheduleService.findById(scheduleId);

		List<Long> reservedSeatIdList = reservedSeatService.findReservedIdListByScheduleId(schedule.getId());
		return ResponseEntity.ok(seatService.findRemainSeats(schedule.getTheaterRoom().getId(), reservedSeatIdList));
		}
	}
    
    ...
    
}

 

컨트롤러에서 여러 Service를 호출할 때의 문제점.


  • 위 구조로 변경했지만, 추가적인 결함이 있었다. 여러 서비스를 하나의 트랜잭션으로 묶을 수 없다는 것이었다.
  • 조회의 경우에는 큰 문제가 없을 수 있겠지만, 아래와 같이 여러 개의 서비스를 통해 저장하는 로직이 연속적으로 수행되는 경우 문제가 발생할 수 있는 구조였다.
    • 만약 serviceB.save()가 실패하더라도, serviceA.save()의 결과는 rollback 되지 않는다.
public class SomthingController {

    private final ServiceA serviceA;
    private final ServiceB serviceB;
    private final ServiceC serviceC;

    public SomthingController(ServiceA serviceA, ServiceB serviceB, ServiceC serviceC) {
    	this.serviceA = serviceA;
        this.serviceB = serviceB;
        this.serviceC = serviceC;
    }
    
    @GetMapping("/")
    public void something() {
    	serviceA.save();
        serviceB.save();
        serviceC.save();
    }

}

 

두 번째 시도 - 여러 서비스를 관리하는 Service


  • DAO와 1:1 매핑하는 Service는 유지한 채로, 여러 개의 Service를 조합해서 사용하는 통합 서비스를 구현했다.
    • 여러 개의 서비스를 의존받아 별도의 비즈니스 로직을 작성한다.
    • 이 서비스는 재사용성보다는 편의성을 위해 정의한 Service로 활용한다.
@Service
@Transactional(readOnly = true)
public class ReservationService {

	private final ReservedSeatService reservedSeatService;
	private final SeatService seatService;
	private final ScheduleService scheduleService;

	public ReservationService(ReservedSeatService reservedSeatService,
		SeatService seatService, ScheduleService scheduleService) {
		this.reservedSeatService = reservedSeatService;
		this.seatService = seatService;
		this.scheduleService = scheduleService;
	}

	/**
	 * 스케줄 별 예약좌석 조회
	 *
	 * @param scheduleId 스케줄 id
	 * @return 예약(되어있는) 좌석의 id 리스트
	 */
	@Transactional
	public List<ResponseFindSeat> findReservePossibleSeatList(Long scheduleId) {
		Schedule schedule = scheduleService.findById(scheduleId);

		List<Long> reservedSeatIdList = reservedSeatService.findReservedIdListByScheduleId(schedule.getId());
		return seatService.findRemainSeats(schedule.getTheaterRoom().getId(), reservedSeatIdList);
	}
}

1:1 매핑 Service의 문제점(?)


  • 기존에는 Entity를 외부로 노출시키지 않고자, 모든 Service에서 Dto객체를 반환했지만, 1:1 매핑 서비스를 거치도록 하면 Entity를 반환하는 public메서드를 제공한다는 취약점(?)이 존재했다.
  • 예를 들어 findById와 같은 함수도 서비스를 통해 호출했다.
    • 서비스를 거치게 한 이유는 Optional에 대한 처리(예외 메시지)를 공통적으로 처리하고 싶었기 때문이다.
    • 만약 1:1 매핑 Service를 거치지 않고 Repository를 직접 접근한다면, orElseThrow 이후의 로직이 통일되지 않는다는 점이 마음에 들지 않았다.
@Service
public class SomeEntityService {

	private final SomeEntityRepository;

	public SomeEntity findById(Long id) {
		return SomeEntityRepository.findById(id)
			.orElseThrow(() -> new NoSuchElementException("SomeEntity FindById Exception Message"));
	}

 

그럼 어떻게 짜는 것이 옳을까..


정답은 없다. 참조를 통해 나만의 기준을 정리해보았다.

 

Q: 엔티티의 변경이 controller layer까지 영향을 안 주기 위해 service 계층에서 엔티티를 반환하지 않도록 하는 게 실무에서는 크게 이점이 없다는 말씀으로 이해하면 될까요?

A: 네 이미 대부분의 계층이 엔티티에 의존하는데, 컨트롤러 계층에서 단순히 엔티티를 모른다고 해서 얻는 이점은 적습니다. 다만 강의에서 설명드렸듯이, 엔티티를 API로 외부에 공개하는 것은 좋지 않습니다.

왜 꼭 서비스를 통해서 리포지토리에 접근해야 하지?라는 근본적인 의문을 품는 것이 필요합니다.

 

우선은 1:1 매핑 Service의 목적은 Repository에 대해 공통적인 접근을 제공하기 위한 목적으로 정의해서 사용한다.

이 과정에서 Entity를 반환할 수 있지만, Controller에서 직접 Entity를 반환하도록 사용하는 것은 주의하자.

 

항상 1:1 매핑 Service를 거치는 것은 아니다. 단순한 저장과 같은 로직들은 직접 Repository를 사용하여 유연하게 사용해도 괜찮다.

 

코드의 중복은 줄이고 일관성 있게 사용할 수 있도록.. 하지만 유연하게 사용하자

 

참조


반응형