Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- ObjectOptimisticLockingFailureException
- java
- multimodule testcontainers
- 멀티모듈 테스트컨테이너
- DI
- S3
- TestContainers
- springsecurity
- AccessToken
- 알고리즘
- RefreshToken
- redissonlock aop
- 낙관적 락 재시도
- OptimisticLock
- jpa
- spring aop
- 백준
- 낙관적 락 롤백
- 소수찾기 java
- kotest testcontainers
- spring DI
- 우아한 테크러닝
- aop
- Spring Cloud Gateway
- Invalid property 'principal.username' of bean class
- interface
- @transactional
- netty
- ObjectOptimisticLockingFailureException 처리
- 형상관리
Archives
- Today
- Total
조급하면 모래성이 될뿐
Service에서 다른 Service를 의존하게 된다면 ? 본문
문제 상황
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 메서드의 역할이 너무 많아졌다.
- 현재 예매된 좌석 리스트를 찾은 후, 예매 가능한 좌석 정보를 반환한다.
- ReservedSeatService가 ReservedSeatRepository와 1:1 매핑이 되는 Service로 사용되지 않는다.
첫 번째 시도 - 의존성을 컨트롤러에서 받아서 처리하기
먼저 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를 사용하여 유연하게 사용해도 괜찮다.
코드의 중복은 줄이고 일관성 있게 사용할 수 있도록.. 하지만 유연하게 사용하자
참조
반응형
'TroubleShooting > 데브코스' 카테고리의 다른 글
Service에서 DataIntegrityViolationException을 Catch 못함 (0) | 2022.07.14 |
---|---|
S3 파일 업로드 (0) | 2022.07.11 |
N + 1 직접 만났다 (0) | 2022.07.01 |
Spring Data Jpa Insert 할 때 Select가 나가네.. (0) | 2022.07.01 |
JPQL like에 _ 포함시키기 (0) | 2022.07.01 |