프로젝트의 목적
지난 3년간 업무에서 접했던 코드들은 객체 지향과는 거리가 멀었다.
Service단이나 DAO는 추상화가 되어있었지만 대부분의 데이터는 Map으로 관리해야 했다.
자바라는 언어에 관심을 갖게 된 계기가 '객체 지향'이었기 때문에 마음속엔 항상 아쉬움이 있었다.
Map으로 데이터를 관리하는 건 어떨까?
예를 들어 customer에 name, address, phone number라는 속성이 있다고 하자.
컨트롤러는 화면으로부터 customer 정보가 담긴 Map 타입 파라미터를 받는다.
컨트롤러에서 customer가 가진 속성을 확인하려면 디버깅을 하거나 for문으로 Map 내부를 모두 print 해봐야 한다.
그뿐만이 아니다.
실수로 map.get("nmae")으로 오타를 내서 에러가 날 수도 있고, birthday라는 속성이 있다고 착각하여 에러가 날 수도 있다.
age라는 속성을 추가하려면 Map에서 속성을 꺼내는 코드마다 전부 추가해줘야 한다.
age라는 속성을 추가했는데도 화면에서 age를 추가하지 않았다면 역시 에러가 발생한다.
Map으로 데이터를 관리하는 게 적절할 때도 있다.
하지만 위와 같이 customer라는 도메인을 관리할 경우 개발 생산성이 좋지 않고 화면단 코드에 종속적이다.
그렇다면 객체로 만들면 될 일이다.
이 부분에 대해서 고민을 많이 해봤다. 하지만 이미 오래된 프로젝트에서 나 혼자 통일성을 헤치는 것은 적절하지 않다는 결론을 내렸다.
또한 전체적으로 리팩토링을 하려면 공수가 들테고, 업무 환경상 그건 내 의지만으로 될 영역이 아니었다.
그래서 이번 기회에 객체 지향 설계를 해보기로 했다.
참고한 책은 <객체지향의 사실과 오해>, <토비의 스프링 3.1>, <자바 ORM 표준 JPA 프로그래밍>이다.
참고한 강의는 <실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발>이다.
구현 코드
서비스 내용과 구조
- 메뉴
- 포인트
- 주문 - 포인트로 결제
- 특정 기간 내 인기 메뉴 조회
간단한 커피 주문 서비스이다.
도메인 단위로 구조를 설정했다.
실제 서비스라면 주문 서비스에 트래픽이 몰릴 거라 (혼자서..) 가정을 해보았다.
그런 상황에서 주문 서비스만 스케일 업하거나 또는 장애가 번지는 걸 방지하기 위해서 도메인 단위 구조가 적절하다고 생각했다.
이런 구조로 한 번 설계해보고 싶었기도 했고.
적용한 도메인 단위 구조는 application / domain / interfaces 레이어로 나뉜다.
- application
- Service
- 복잡한 비즈니스 로직이 있는 곳
- domain
- Entity, Repository
- 도메인 정보가 있는 곳, 실제 DB 데이터 처리 로직을 담당하는 곳
- interfaces
- Controller, DTO
- 사용자와 요청, 응답을 주고 받는 곳
- infrastructure
- 이 프로젝트에선 제외했다
- 영속성을 구현한(implements) DAO와 외부와 통신하는 기능이 있는 곳
다만 학습용 프로젝트임을 고려하여 도메인마다 프로젝트를 생성하는 대신 패키지로 구분하였다.
(사실상 package-by-feature에 가깝다)
구조를 설정하면서 마지막까지 고민했던 건 "exception과 util의 경우 공통 기능과 특정 서비스에서만 사용되는 기능을 어떻게 분리해야 하나?"였다.
- 각 서비스에서 exception이 발생할 때 exception 서비스와 통신해야 하나?
- 아니면 공통 기능도 각 서비스마다 넣어줘야 하는 건가?
- 공통 기능이 변질될 가능성이 있고 통일성을 잃어 나중에 큰 문제가 생길지도 모른다
일단 공통 기능은 패키지로 빼놨고 특정 서비스에서만 사용되는 기능은 해당 패키지 하위에 생성했다. (order > exception)
나중에 이런 구조에서 exception이나 util 등을 어떻게 처리하는지 알아봐야겠다.
구성은 아래 글을 참고하였다.
https://brunch.co.kr/@cg4jins/7
설계
객체지향 설계의 첫번째 목표는 훌륭한 객체를 설계하는 것이 아니라 훌륭한 협력을 설계하는 것이라는 점을 잊지 말자.
<객체지향의 사실과 오해>
손님이 할 일은 커피를 주문하는 것이다.
하지만 손님 혼자서는 커피를 주문할 수 없다.
- 어떤 커피를 파는지 모른다
- 커피의 가격을 모른다
- 커피 만들 줄 모른다
이 때 필요한 것이 협력이다.
손님은 할 일을 하기 위해 누군가에게 요청을 하거나 받기도 한다.
서로 요청을 하고 받아가며 각자의 일을 해내는 행위가 바로 협력이다.
메뉴
손님은 어떤 커피를 파는지 물어볼 만한 대상을 찾는다.
커피 메뉴를 알려주는 일을 담당하는 메뉴라는 객체에게 메뉴를 알려달라고 요청한다.
메뉴는 커피 메뉴명과 가격을 알아와 손님에게 전달해 준다.
- 전체/단건 조회
- 메뉴 생성
포인트
손님은 먹고 싶은 커피를 하나 골랐다.
근데 커피는 포인트로 결제되므로 포인트를 충전해야 한다고 한다.
손님은 자신의 정보와 돈을 주며 자기만 쓸 수 있는 포인트를 만들어 달라고 한다.
포인트 객체는 손님의 정보와 돈이 담긴 포인트를 만들어준다.
- 포인트 생성
- 포인트 충전
- 포인트 조회
- 포인트 결제
주문
포인트도 충전한 손님은 이제 커피 주문을 하려 한다.
손님은 자신의 정보와 자신이 고른 커피 정보들을 주문 객체에 알려주고 주문 요청을 한다.
주문 객체는 받은 정보를 가지고 주문을 생성한다.
주문 객체는 결제를 하고 싶지만 혼자서는 못 한다. 주문만 하기 때문이다.
-> 포인트 객체에게 손님의 정보와 손님이 고른 커피의 총금액을 알려주고 결제 요청을 한다.
주문 객체는 결제가 완료되면 주문 내역도 발송하고 싶다. 역시 혼자서는 못한다.
->주문 내역 객체에게 발송을 요청한다.
- 주문 생성
- 포인트에게 결제 요청
- 주문 내역 객체에게 발송 요청
public Long order(OrderDto orderDto) throws JsonProcessingException {
Customer customer = customerRepository.findById(orderDto.getCustomerId()).orElseThrow(CustomerNotFoundException::new);
List<OrderItem> orderItems = orderDto.getOrderItems().stream()
.map(oid -> {
Menu menu = menuRepository.findById(oid.getMenuId()).orElseThrow(IllegalStateException::new);
return OrderItem.builder()
.menu(menu)
.counts(oid.getCounts())
.build();
})
.collect(Collectors.toList());
Order order = Order.createOrder(customer, orderItems);
// 주문 생성 ○
orderRepository.save(order);
// 결제 ●
pointService.usePoint(customer.getId(), order.getTotalPrice());
// 주문 내역 발송 ●
OrderDto orderDtoToSend = OrderDto.OrderToDto(order);
List<ProducerRecord<String, String>> orderHistory = orderHistoryService.send(orderDtoToSend);
return order.getId();
}
주문 객체는 자신만이 맡은 일(○) 말고도 요청할 일(●)도 많다.
주문 생성 - 결제 - 결제 내역 발송을 한 트랜잭션으로 묶으려고 이렇게 설계했다.
- 하지만 객체 지향 설계의 관점에서 결합도가 강하다는 문제점이 명확히 보인다.
- 결제 내역 발송 서비스는 외부 API와 통신한다. 통신에 문제가 생길 경우 주문 서비스에도 영향을 미친다.
- 각 서비스끼리(order <-> point) API로 통신한다고 가정했을 때도 마찬가지다.
결합도를 느슨하게 할 필요가 있다.
주문 내역
주문 서비스에서 주문 서비스만큼이나 중요한 게 주문 내역이라고 생각하기 때문에 장애가 나도 손실 위험이 적은 메세징 시스템을 적용했다.
테스트 용으로만 구현해서 객체 없이 Service만 제공한다.
- 주문 내역을 메세징 시스템에게 발송
인기 메뉴 조회
손님은 이 카페에서 7주일 동안 인기가 많았던 커피들을 알고 싶다.
손님은 인기 메뉴 조회에게 인기 메뉴 조회를 요청한다.
- 특정 기간 내 인기 메뉴 조회
메뉴 객체가 있는데 인기 메뉴 객체를 따로 만들어야 하나 고민이 있었다.
통계성 데이터이므로 따로 객체를 만들 필요가 없다고 판단했기 때문에 객체를 만들지 않았다.
이렇게 해서 커피 주문 서비스 설계를 끝냈다.
'단일 책임 원칙'은 비교적 잘 지켰지만 '느슨한 결합'에 있어서는 부족한 부분이 많았다.
어디까지나 학습용 프로젝트라는 걸 떠올리며 간결하게 설계했다.
https://onibmag.tistory.com/36
실제 서비스를 설계한다면 확장 규모를 가늠해 보고 추상화하여 설계해야겠지.
예를 들면 주문을 인터페이스를 두어 모바일 주문/키오스크 주문/직접 주문 객체로 구현할 수도 있겠다.
접할 기회가 거의 없었던 기술들이다 보니 처음엔 갈피도 못 잡았지만 그래도 새로운 걸 알아가는 재미로 잘 마무리하였다.
'Project' 카테고리의 다른 글
2. JAVA로 아주 간단한 WAS와 Spring MVC Framework 만들기 (0) | 2023.01.07 |
---|---|
1. JAVA로 아주 간단한 WAS와 Spring MVC Framework 만들기 (0) | 2023.01.04 |
4. AWS + Spring Boot + React 프로젝트 근데 이제 배포 자동화를 곁들인 (0) | 2022.03.27 |
3. AWS + Spring Boot + React 프로젝트 근데 이제 배포 자동화를 곁들인 (0) | 2022.03.25 |
2. AWS + Spring Boot + React 프로젝트 근데 이제 배포 자동화를 곁들인 (0) | 2022.03.24 |