의문 발생
팀 프로젝트를 진행하면서 내가 담당한 부분에 대한 의문이 발생하였다.
해당 소스 코드 부분은 이렇다.
// 메뉴 단일 조회
@GetMapping("/{menuItemId}")
public ResponseEntity<MenuItemResponseDto> searchMenuItem(@PathVariable UUID menuItemId) {
return menuItemService.searchMenuItem(menuItemId);
}
// 레스토랑의 메뉴 전체 조회
@GetMappring("/{restaurantId}")
public ResponseEntity<List<MenuItemResponse>> searchMenuItemByRestaurant(@PathVariable UUID restaurantId){
return menuItemService.searchMenuItembyRestaurant(restaurantId);
}
코드를 확인하면 두 가지 search 기능을 구현해야 하지만 엔드포인트에서 ID값이 PathVariable로 전달되는 경우
menuItemId 인지, restaurantId인지 구분이 불가능하다.
따라서 searchMenuItemByRestaurant() 의 엔드포인트를 수정하여야 하는 상황이다.
이때 어떻게 엔드포인트를 지정할 지 고민하였지만 URL에 restaurant이 들어가게 된다면
RESTful하지 않은 소스코드라고 판단했다.
RESTful한 구성을 위하여 우선 설계 원칙에 대해 알아보았다.
RESTful 엔드포인트 설계 원칙
✅ 1) 리소스를 기반으로 설계
- 엔드포인트(URL)에는 행위(verb)가 아닌 리소스 명사(noun) 를 사용한다.
- 예:
- ❌ GET /getUsers → 잘못된 방식 (동작을 포함)
- ✅ GET /users → 올바른 방식 (리소스를 표현)
✅ 2) HTTP 메서드 활용
HTTP 메서드를 적절히 활용하여 CRUD(Create, Read, Update, Delete) 작업을 수행해야 한다.
동작HTTP 메서드엔드포인트 예시
| 조회 | GET | /users (전체 조회), /users/{id} (단일 조회) |
| 생성 | POST | /users (새로운 사용자 생성) |
| 수정 | PUT | /users/{id} (전체 업데이트) |
| 부분 수정 | PATCH | /users/{id} (일부 필드 수정) |
| 삭제 | DELETE | /users/{id} (사용자 삭제) |
✅ 3) 계층적인 구조
- 엔드포인트는 계층적 관계를 반영해야 한다.
- 예:
- 특정 사용자의 주문 목록 조회: GET /users/{id}/orders
- 특정 주문의 상세 정보 조회: GET /users/{id}/orders/{orderId}
✅ 4) 쿼리 파라미터 활용
- 검색, 필터링, 정렬과 같은 기능을 추가할 때는 쿼리 파라미터를 사용한다.
- 예:
- GET /products?category=electronics&sort=price_desc
- GET /users?age=30&status=active
✅ 5) 상태를 포함하지 않는 Stateless 방식
- RESTful API는 Stateless(무상태성) 를 유지해야 한다.
- 즉, 각 요청은 서버에 클라이언트의 상태를 저장하지 않고 독립적으로 처리되어야 한다.
✅ 6) 응답 상태 코드 활용
- HTTP 상태 코드를 활용하여 클라이언트에게 적절한 응답을 전달해야 한다.
- | 200 OK | 성공적인 요청 처리 |
- | 201 Created | 새로운 리소스 생성 완료 |
- | 400 Bad Request | 잘못된 요청 |
- | 401 Unauthorized | 인증 실패 |
- | 403 Forbidden | 접근 권한 없음 |
- | 404 Not Found | 요청한 리소스 없음 |
- | 500 Internal Server Error | 서버 내부 오류 |
해결 과정
내가 주목해야할 부분은 MenuItem과 Restaurant의 관계였다.
아래는 MenuItem과 Restaurant 엔티티이고 ManyToOne 관계로 매핑되어 있다.
여기서 알 수 있는 정보는 두 엔티티는 계층적인 관계이며
MenuItem은 Restaurant에 종속되는 하위 엔티티라는 정보이다.
public class MenuItem extends TimeStamped {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(updatable = false)
private UUID id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private int price;
@Column(nullable = false)
private boolean available;
@ManyToOne
@JoinColumn(name = "restaurant_id")
private Restaurant restaurant;
public MenuItem(String name, Integer price, Restaurant restaurant) {
this.name = name;
this.price = price;
this.restaurant = restaurant;
}
public void update(MenuItemRequestDto requestDto) {
this.name = requestDto.getName() == null ? this.name : requestDto.getName();
this.price = requestDto.getPrice() == null ? this.price : requestDto.getPrice();
this.available = requestDto.getIsAvailable() == null ? this.available : requestDto.getIsAvailable();
}
}
public class Restaurant extends TimeStamped{
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "restaurant_id", columnDefinition = "uuid", updatable = false, nullable = false)
private UUID id; // 고유 식별자
@Column(name = "restaurant_name",nullable = false, length = 100)
private String name; // 가게 이름
@ManyToOne
@JoinColumn(name="category_id",nullable = false)
private Category category; // 카테고리
@ManyToOne
@JoinColumn(name="location_id",nullable = false)
private Location location; // 위치
@ManyToOne
@JoinColumn(name="id",nullable = false)
private User owner; // 소유자
@Column(nullable = false)
private Boolean status; // 가게 상태 (운영 중 여부)
}
이 점을 인지하고 RestaurantController에 MenuItem와 관련된 API를 선언하느냐? 그건 아니었다.
예시로 아래와 같은 코드가 작성 될텐데 MenuItemService는 MeneItem을 관리하는 서비스이기 때문에
RestaurantController에 직접 선언하게 된다면 RestaurantController 역시 MenuItem을 관리하는 책임이 생긴다.
즉, 자바의 단일 책임 원칙을 어기게 되는 소스코드이다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/restaurants")
public class RestaurantController {
private final MenuItemService menuItemService;
@GetMapping("/{restaurantId}/menus")
public ResponseEntity<List<MenuItemResponseDto>> searchMenuItemsByRestaurant(@PathVariable UUID restaurantId) {
return menuItemService.searchMenuItemsByRestaurant(restaurantId);
}
}
그렇다면 이 부분을 어떻게 해결하느냐? 그것은 RestaurantService에 MenuItemService를 선언하는 것이다.
이렇게 선언을 한다면 RestaurantController는 MenuItem 관리에 대한 책임을 지지 않기 때문에
단일 책임 원칙을 준수하는 소스코드가 되고 안정적으로 레스토랑의 메뉴를 조회할 수 있다.
@Service
@RequiredArgsConstructor
public class RestaurantService {
private final MenuItemService menuItemService;
// 특정 레스토랑의 전체 메뉴 조회 (메뉴 아이템 서비스 호출)
public List<MenuItemResponseDto> getRestaurantMenus(UUID restaurantId) {
return menuItemService.getMenuItemsByRestaurant(restaurantId);
}
}
그렇다면 여기서 한 가지 더 의문이 생기는데, RestaurantService에서 MenuItem을 호출하는 것은
자바의 단일 책임 원칙을 어기는 소스코드가 아닌가?
이 부분의 해답은 MenuService가 이미 MenuItem 관련 비즈니스 로직을 캡슐화 하고 있고
Restaurant이 MenuItem을 소유하는 개념이므로 자바의 단일 책임 원칙을 어기는 코드가 아닌 것이다.
여기서 꼬리 물기로 더 들어간다면 이런 의문이 다시 생겨난다.
'이미 MenuItemService가 캡슐화 되어 있으니 RestaurantController에 선언하여도 괜찮은 것 아닌가?'
이 의문의 본질은 Controller 계층과 Service 계층 간의 역할분리와
도메인 간의 역할 분리에 따라 달라진다고 한다.
Controller는 해당 도메인에 대한 요청만을 Service계층이 비즈니스 로직을 처리하도록 하여야 하며
타 도메인의 Service가 비즈니스 로직을 처리하게 한다면 그것은 단일 책임 원칙을 어기는 것이다.
하지만 해당 도메인의 Service 계층이 타 Service 계층과 함께 비즈니스 로직을 처리하는 것은
협업에 해당하여 단일 책임 원칙을 어기는 것이 아니라고 판단하며 타 도메인의 응답을 보내는 것 또한
컨트롤러가 요청을 받고 응답을 준비하는 것에만 집중하고 있으며,
비즈니스 로직은 해당 도메인의 Service 계층에서 처리하고 있기 때문에 단일 원칙 책임을 지키고 있다는 내용이다.
해결 방안
다시 본래의 문제로 돌아와서 해결 방안은 레스토랑의 메뉴를 조회하는 API는 RestaurantController에서 선언하되,
RestaurantService에서 MenuItemService를 호출하여 단일 책임 원칙과 RESTful에 대한 원칙을 지키는 것이다.
'PROJECT > Delivery' 카테고리의 다른 글
| README 파일 첨부용 이미지 (0) | 2025.02.25 |
|---|---|
| 순환참조 해결을 위한 중간 서비스 활용 (1) | 2025.02.25 |
| ResponseDto 페이지 응답 (1) | 2025.02.14 |