💋 인트로
안녕하세요. 우아한테크코스 5기 깃짱이라고 합니다.
리팩터링 미션을 통해서, 메뉴, 주문, 테이블
로 이루어져 있는 코드를 객체 지향 관점에서, 의존성의 관점에서, 그리고 그 연장선으로 모듈로 분리하게 되는 과정까지의 리팩터링 경험을 하게 되었습니다.
처음에는 단지 손가락 노동 뿐인 귀찮은 미션이라고 생각했는데, 미션 진행할 수록 객체지향에 대해서 제가 가지고 있던 일종의 강박 같은 것들을 깨뜨렸습니다. 단지 어떤 코드가 안좋다는, 근거 없이 느낌적인 느낌으로만 내리던 말도 안되는 판단을 버리고, 어떤 코드가 정말로 좋은 코드인지에 대해서 상황을 생각하며 더 입체적으로 판단을 내릴 수 있게 되었습니다.
💋 1단계: 테스트를 통한 코드 보호
1단계 Pull Request: https://github.com/woowacourse/jwp-refactoring/pull/481
✔️ 리팩터링의 정의
리팩터링을 검색하면 아래와 같은 정의를 찾을 수 있습니다.
리팩터링(refactoring)은 소프트웨어 공학에서 '결과의 변경 없이 코드의 구조를 재조정함'을 뜻한다.
리팩터링은 ‘결과의 변경이 없다’는 것이 포인트인데, 결과의 변화가 없다는 것을 보장할 수 있으려면 어떻게 해야 할까요? 이제까지 만든 모든 기능들을 다 돌려보면서 확인해볼 수 있을 겁니다. 하지만, 분명 사람이기에 모든 기능들이 제대로 동작한다고 확인하는 과정에는 실수가 포함될 수 있습니다. 그렇기에 우리는 테스트 코드를 작성합니다. 리팩터링을 하면서 코드의 구조는 재조정되었는데도, 이제까지 작성한 테스트 코드가 모두 통과한다면 리팩터링의 정의 그대로 ‘결과의 변경 없이 코드의 구조를 재조정’했다고 볼 수 있을 것입니다. 테스트 코드의 중요성은 매우 커서 리팩터링에 대해서, 극단적으로 테스트 코드가 없으면 리팩터링이 아니라는 입장도 존재합니다.
✔️ 서비스 로직 파악부터 확실히 하는 게 더 빠르다!
미션의 요구사항은 크게 오직 하나였습니다. 테스트 코드를 작성한다.
사실, 테스트 코드 작성은 은근히 귀찮은 일 중 하나이기 때문에 얼른 해치우고 싶은 마음이 들어서 빠르게 테스트 코드를 작성해 나갔습니다. 단순히 CRUD에 대한 부분이라면 정말 빠르게 작성할 수 있습니다. 하지만, 검증 로직들이 정말 많았습니다. 또 객체의 생명 주기와 관련해서도 연관성 있는 객체들이 존재했습니다. 예를 들어서, 메뉴(Menu
)는 생성 시에 해당 메뉴에 어떤 상품(MenuProduct
)들이 포함되어 있을지 설정해야 했는데, 이렇게 되면 Menu
와 MenuProduct
은 동일한 생명 주기를 가진다고 볼 수 있습니다.
테스트 코드 작성을 빨리 해치우려다가 오히려 도르마무에 빠진 듯한 느낌을 받아서, 서비스 로직을 확실히 파악하면서 요구 사항을 작성했습니다.
## 요구 사항
- 메뉴 그룹
- 메뉴 그룹을 생성할 수 있다.
- 메뉴 그룹은 아이디와 이름을 가질 수 있다.
- 메뉴 그룹 리스트로 확인할 수 있다.
- 메뉴
- 메뉴를 생성할 수 있다.
- 메뉴는 아이디, 이름, 가격, 메뉴 그룹, 메뉴에 속하는 상품을 가질 수 있다.
- 메뉴는 0 이상의 가격을 가진다.
- 메뉴는 반드시 메뉴 그룹에 속해야 한다.
- 메뉴의 가격은 메뉴에 속하는 상품 * 수량의 합 이하여야 한다.
- 메뉴를 리스트로 확인할 수 있다.
- 주문
- 주문을 생성할 수 있다.
- 주문 항목은 반드시 1개 이상 존재해야 한다.
- 동일한 메뉴는 1개의 주문항목으로 표현된다. (예. 스키야키 2개)
- 주문한 테이블이 존재하지 않으면 예외가 발생한다.
- 주문 시 주문 상태는 `COOKING`으로 변경된다.
- 주문을 리스트로 확인할 수 있다.
- 주문 상태를 변경할 수 있다.
- 주문 상태가 `COMPLETION`인 주문은 상태를 변경할 수 없다.
- 상품
- 상품을 생성할 수 있다.
- 상품의 가격은 0원 이상이어야 한다.
- 상품을 리스트로 확인할 수 있다.
- 테이블
- 테이블을 생성할 수 있다.
- 테이블을 조회할 수 있다.
- 테이블을 `EMPTY` 여부를 변경할 수 있다.
- 단체 테이블 중 하나의 테이블은 비울 수 없다.
- 주문 상태가 `COOKING`, `MEAL`인 테이블은 비울 수 없다.
- 테이블에 앉은 사람의 수를 변경할 수 있다.
- 테이블에 앉은 사람의 수는 항상 0명 이상이어야 한다.
- `EMPTY`인 테이블의 사람 수는 변경할 수 없다.
- 테이블 그룹
- 테이블 그룹을 생성할 수 있다.
- 테이블 2개 이상 묶어서 단체 테이블로, 테이블 그룹을 지정할 수 있다.
- 그룹으로 묶이는 테이블은 모두 비어있는 상태여야 한다.
- 그룹으로 묶이는 테이블들은 이미 존재하는 테이블 그룹이 없어야 한다.
- 단체로 지정되는 테이블은 `EMPTY` 여부가 `false`가 된다.
- 테이블의 그룹을 해제할 수 있다.
- `COOKING`, `MEAL` 상태인 테이블은 그룹을 해제할 수 없다.
- 그룹을 해제하더라도, `EMPTY` 여부는 `false`이다.
와중에도 요구 사항은 최대한 너무 기술적이지 않고, 비즈니스를 나타낼 수 있도록 작성하도록 노력했는데, 리뷰어였던 코코닥이 바로 알아차려 주어서 매우 기뻤습니다.
✔️ 나의 테스트 코드 작성
나는 평상시에 ‘인수테스트’를 작성했을 때, 가장 내가 작성한 기능에 대해 신뢰를 느끼고 편안했기 때문에 인수 테스트를 선택했습니다.
신나게 오로지 성공 케이스
에 대해서만 인수 테스트를 작성했더니, 위와 같은 피드백을 받았습니다. 아차 싶으면서도 사실 서비스에서 ‘성공’은 너무 당연한 것이고, 완성도 높은 서비스일 수록 ‘실패’나 ‘예외 상황’에 대한 처리가 굉장히 꼼꼼해 져야 된다는 것을 다시 한 번 생각하게 되었습니다.
💋 2단계: 서비스 리팩터링
2단계 Pull Request: https://github.com/woowacourse/jwp-refactoring/pull/583
✔️ 기존 코드의 문제점
2단계는 사실 이제까지 했던 가장 익숙한 리팩터링이었습니다. 객체에 메세지를 보내서, 객체가 일하도록 하라는 객체지향의 가장 기본적인 원칙에 충실히 리팩터링을 진행했습니다.
초기에 주어졌던 코드의 문제점은 아래와 같았습니다.
- Request, Response를 받는 객체로 도메인 객체를 그대로 사용중이었습니다.
- 모든 검증에 대한 과정은 서비스 로직에서 수행중이었습니다.
✔️ 1. JdbcTemplate를 사용하던 코드를 JPA를 사용하도록 변경
JPA를 사용하도록 변경한 데는 큰 뜻은 없었습니다. 단지 요구사항에서 JPA에 대한 언급이 있어서 변경하게 되었습니다.
JPA로 변경하게 되면서 가장 많이 봤던 에러는 연관관계가 맺어져 있는 객체를 생성할 때 그 필드에 있는 객체를 영속화하지 않았기 때문이었습니다.
양방향으로 연관되어 있는 객체를 언제 영속화 해주어야 할 지에 대해서 많이 고민하다가 결국에는 cascade 옵션으로 해결할 수 있었는데, 아직은 좋은 해결책인지는 잘 모르겠습니다.
✔️ 2. Request, Response 객체와 DTO 객체 생성해 도메인과 분리
이 부분은 처음에는 꼭 필요한가 싶어서 하지 않으려고 했다가, 도메인에 조그만 한 변경만 가하면 제가 1단계에서 작성했던 26개의 테스트가 모두 빨간불을 터뜨리기 시작해서 어쩔 수 없이 하게 되었습니다.
사실 우테코 레벨2 동안에는 왜 대체 불편하게 Request 객체를 분리하라고 하는 건지 제대로 이해하지 못했었는데, 이번 미션을 통해서 확실히 인지하게 되었습니다.
도메인 로직을 변경될 때마다 모든 계층이 울부짖는 건 진짜 너무 심리적으로 힘들었습니다. 뭐 하나 진행하려 해도 컴파일 에러 다 잡고 나면 뭘 하려고 했던 건지 까먹는 지경ㅋㅋㅌㅋㅌ 무튼 다 변경했습니다.
✔️ 3. 비즈니스 로직을 도메인 내로 이동
요구사항의 힌트 중에 아래와 같은 내용이 있었습니다.
모델에 getter, setter 메서드를 무조건 추가하는 것은 좋지 않은 버릇이다. 특히 setter 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다. setter 메서드의 또 다른 문제는 도메인 객체를 생성할 때 완전한 상태가 아닐 수도 있다는 것이다. 도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다.
저도 평소에 깊이 공감하고 있는 내용이었기 때문에 거의 대부분의 검증 로직들은 도메인의 생성 시점으로 넣었습니다. validation과 관련된 메서드들을 각자의 엔티티 안에 넣고, 최대한 생성자에서 검증하는 식으로 변경했습니다.
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
private Long orderTableId;
private String orderStatus;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "order_table_id")
private OrderTable orderTable;
@Enumerated(STRING)
private OrderStatus orderStatus;
private LocalDateTime orderedTime;
@OneToMany(mappedBy = "order")
private List<OrderLineItem> orderLineItems;
private Order(final Long id, final OrderTable orderTable, final OrderStatus orderStatus, final LocalDateTime orderedTime, final List<OrderLineItem> orderLineItems) {
validateOrderLineItems(orderLineItems);
validateEmptyOrderTable(orderTable);
this.id = id;
this.orderTable = orderTable;
this.orderStatus = orderStatus;
this.orderedTime = orderedTime;
this.orderLineItems = orderLineItems;
}
// ...
}
정말 불가피하게 생성 시에 결정되지 않는 필드들이 존재했는데, JPA의 엔티티 클래스가 아니었다면 아마 새로운 더 넓은 클래스로 감아 버렸을 테지만, 현재 코드에서 너무 변경이 많다고 생각되어 이 경우에만 setter를 열어 두고, setter 내에서 검증 로직을 넣어 주었습니다.
public void changeOrderStatus(final OrderStatus orderStatus) {
if (!canChangeOrderStatus()) {
throw new IllegalStateException("[ERROR] 이미 완료된 주문은 상태를 변경할 수 없습니다.");
}
this.orderStatus = orderStatus;
}
public void updateOrderLineItems(final List<OrderLineItem> orderLineItems) {
validateOrderLineItems(orderLineItems);
this.orderLineItems = orderLineItems;
}
이런 식으로 검증 관련 로직을 모두 도메인에 넣게 되면, 아래와 같이 서비스 로직이 굉장히 가벼워 집니다.
(왼쪽인 서비스 로직에서 검증할 때, 오른쪽이 도메인에서 검증할 때)
분명히 굉장히 만족스러운 코드였습니다. 조영호 개발자의 우아한 객체지향을 보기 전까지는….
💋 3단계: 의존성 리팩터링
3단계 Pull Request: https://github.com/woowacourse/jwp-refactoring/pull/770
✔️ 조영호 개발자의 우아한 객체지향을 보고…
조영호 개발자의 우아한 객체지향이라는 영상을 보고 꽤 많은 충격을 받았습니다.
2단계를 진행하면서, 객체 내에 검증 로직을 넣기 위해서 대부분의 간접 참조를 객체 참조로 변경했습니다.
객체 참조는 명확한 장단점이 있습니다.
모든 객체가 연결되어 있기 때문에, 모든 객체로 접근이 가능하고, 어떤 객체라도 함께 수정이 가능합니다. 객체 참조는 결합도가 가장 높은 의존성입니다.
단순 메모리 상에서만 돌아가는 객체였다면 별다른 문제가 없을 것입니다. 하지만, 이 객체가 엔티티인 경우에는?
객체 참조로 모든 곳을 갈 수 있다는 말은 즉, 하나의 엔티티를 수정할 때 트랜잭션의 범위는 모든 테이블이 될 수 있다는 말과 같다는 것을 알게 되었습니다.
이외에도 정말 많은 새로운 인사이트를 얻었지만, 아직 제가 직접 체감해본 것은 없어서 당장 이 미션 코드에서 느끼는 문제점만을 바탕으로 수정에 들어갔습니다.
객체간의 연관관계를 어떻게 설정했는지, 흐름에 대해서 설명하겠습니다.
✔️ 1. 생명주기를 공유하는 객체를 제외하고는 모두 id를 참조하는 간접참조로 변경
먼저, 아래의 간단한 규칙으로 객체의 직접 참조 여부를 판단했습니다. 도메인 제약사항 공유라는 규칙은 조금 모호하게 느껴지는 부분이 있어서, 일단 배제했습니다.
함께 만들어져야 하는 객체는 아래와 같이 두 쌍이었습니다.
- Menu와 MenuProduct
- Order와 OrderLineItem
OrderTable과 TableGroup은 위의 둘과는 달리 OrderTable 이 생성될 때는 별도의 TableGroup에 속할 수도, 속하지 않을 수도 있어서 생명주기를 공유하지는 않는다고 판단했습니다.
✔️ 2. 검증 로직은 Validator 객체로 분리
이제까지는 최대한 많은 검증을 도메인 내에서 생성 시에(생성자든, 정적 팩터리 메서드든) 수행하자는 것이 좋다고 생각했습니다.
조영호 개발자의 우아한 객체지향을 듣고나서, 평소와 다른 방식이지만 적용해 보았습니다.
Menu를 만들 때에 Product 내용을 검증해야 하므로, Menu와 Product는 간접참조로 변경했더라도, MenuValidator에서 ProductRepository를 호출해야 했습니다.
여기까지 변경하니 객체 간의 연관관계가 아래와 같이 완성되었습니다.
(그림에 빠뜨렸는데, OrderLineItem이 Menu를 간접참조합니다)
이렇게 하니, 객체지향보다 절차지향적인 면이 강조되기는 했습니다. 객체지향은 여러 객체를 오가면서 검증 로직을 파악해야 하지만, Validator에 전체 검증 로직을 모아 놓으니 한눈에 파악할 수 있게 되었습니다.
물론 명확한 단점도 존재합니다.
분명히 응집도는 높아졌지만, 도메인 검증 로직을 거치지 않고 도메인을 생성할 수 있어서, Validator를 통한 검증을 강제할 수가 없게 되었습니다. 정신 빠진 개발자가 와서 validate를 호출하지 않고 객체를 생성한 뒤에 곧바로 사용하기 시작한다면 어떻게 될까요..?
제 리뷰어였던 코코닥도 비슷한 것을 느낀 것이 신기했습니다.
✔️ 3. 패키지 설계
먼저 조영호 개발자가 언급했던 ‘생명주기’와 ‘도메인 제약사항’을 기준으로 생각해보기로 했습니다.
큰 고민 없이, 동일한 생명주기를 갖는 객체(위에서 말한 두 쌍)만을 가지고 패키지를 나눠볼까, 했더니 6개의 패키지가 나왔습니다. (그림에 빠뜨렸는데, OrderLineItem이 Menu를 간접참조합니다)
이렇게 되면, 객체와 패키지의 개념을 분리할 수 없을 정도로 너무 작게 나눈 것 같아서, 제가 생각했을 때 연관성이 큰 객체끼리 묶기로 했습니다.
이 연관성을 위한 기준으로 다양한 생각을 했었습니다.
- 도메인 로직에서 서로를 많이 호출하는가?
- (여기가 서비스 회사라고 생각했을 때) 하나의 패키지에 하나의 부서를 지정하더라도 그 범위가 너무 크거나, 너무 작지 않은가?
제가 지금 읽어보는데, 정말 지극히 개인적입니다. 원래 이렇게 패키지 설계는 주관적인 걸까요?
제가 서비스 회사를 만든다면, Product랑 Menu는 다른 부서에 넣을 것 같아요. 왠지 Product는 이번 미션에서는 규모가 작았다만, 상품 자체의 정보는 워낙 많고 업데이트도 잦을 것 같아서요.
그리고 Menu, Order 이렇게 크게 분리했습니다. 위에서 말한 기준으로 연관성이 크다고 생각했습니다. 근거는 이 이상 말할 수가 없습니다.
아무튼 그래서 이렇게 패키지를 분리했습니다.
menu 패키지는 product 패키지를 호출하고 있습니다. 어쨌든 Product에 대한 내용을 검증해야 하므로, 저 의존성은 떼낼 수가 없더라고요. 단방향인 것에 만족하기로 했습니다.
💋 4단계: 멀티 모듈 적용
4단계 Pull Request: https://github.com/woowacourse/jwp-refactoring/pull/770
✔️ 멀티 모듈 분리 계획
위에서 말한 기준대로 menu, order, product를 각각의 모듈로 분리할 것입니다. 그리고 이 모든 것을 가져다 사용할 app 모듈을 만들 것입니다.
그리고 애매하게 모든 곳에서 사용중인 BaseDate(모든 엔티티에 created_at, updated_at 자동 설정하는 엔티티), 그리고 이 엔티티를 위한 BaseDateConfig이 애매하게 남더라고요. 이 둘은 다른 크루들은 어떻게 했나 훔쳐봤더니, common 혹은 core 모듈로 많이 분리하길래 저도 따라서 core 모듈로 만들었습니다.
위에서 봤듯이 menu 패키지는 product 패키지를 호출하고 있습니다. 그래서 menu 모듈 역시 product 모듈을 의존성에 추가했습니다.
OrderLineItem이 Menu를 간접참조하고 있어서, order 모듈도 menu 모듈을 의존하게 되었습니다.
패키지 간 의존성이 그대로 모듈 간 의존성과 같아 지더라고요.
✔️ 모듈 간 의존성
app → menu, order, product, core
core → 의존성 X
menu → core, product
order → core, menu
product → core
✔️ 인수테스트는 어디에 놓을까?
인수테스트는 그냥 다 app 모듈에 넣었습니다. 각각의 인수테스트 상황을 연출하기까지는 진짜 많은 객체를 생성해야 하는데, 그렇게 하기 위해서 서로 모두 의존성을 추가해버리면 싱글 모듈로 돌아가버리니깐요!
💋 우테코 마지막 미션 완료!
우테코에 와서 정말 많은 미션을 했던 것 같은데, 이번이 마지막이라 생각하니 시원섭섭 했습니다. 사실 지금은 아직 섭섭하지는 않아서 시원하기만 함.
시간도 많이 들고 귀찮은 미션인 것 같았는데, 배워가는 것도 아주 많아서 제이슨 코치에게도 감사한 마음이 남았습니다. (저번에 제가 아끼는 숫가락 젓가락을 방학 사이에 홀랑 버려 버려서 좀 미워했긴 했지만 ㅎㅎ) 진짜 미션 설계를 기가 막히게 잘 한 것 같다고 생각했습니다. 짱짱
리뷰어였던 코코닥이 너무너무 훈훈한 리뷰를 남겨줘서 감동 받았습니다.
나는 기억도 안나는데, 최종 코테때 나를 봤다고 기억도 해준게 굉장히 감동이었습니다. 그때는 진짜 자바 시작한지 갓 2개월 된 걸음마 단계였는데, 코코닥 같은 고수에게 이렇게 고수로 오해도 사다니 정말 기분이 좋네요 ㅎㅎㅎ
그러면 이제 밀린 4단계 내 미션 회고를 제가 혹시 시간이 남아서 작성하지 않는다면, 레벨4는 이제 보내주게 됩니다. 안녕! 우테코도 곧 안녕ㅜㅜ
생각해보니 이제 내가 개발을 시작한지 오늘이 거의 만 1년이네요. 돌잔치 해야겠어요 ^..^
💋 참고자료
- https://kwonnam.pe.kr/wiki/java/lombok/pitfall
- https://jojoldu.tistory.com/266
- https://www.slideshare.net/baejjae93/ss-151545329
- https://hudi.blog/why-use-multi-module/
- https://medium.com/@jojiapp/gradle-multi-module에서-testfixtures를-이용하여-테스트-코드-중복-줄이기-3a4737f574f
- https://kwonnam.pe.kr/wiki/gradle/multiproject
- https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:adding_subprojects
- https://hyeon9mak.github.io/woowahan-multi-module/
- https://techblog.woowahan.com/2637/
- https://www.youtube.com/watch?v=nH382BcycHc
- https://toss.tech/article/how-to-manage-test-dependency-in-gradle
도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'우아한테크코스5기' 카테고리의 다른 글
[우테코] Domain Driven Development(도메인 주도 개발)이란? (3) | 2023.10.17 |
---|---|
[우테코] 점진적 리팩터링: 달리는 기차의 바퀴를 갈아끼우기 (0) | 2023.09.19 |
[우테코] 톰캣(HTTP Server) 구현하기 미션 회고 (0) | 2023.09.15 |
[우테코] 레벨3 레벨인터뷰: 인터뷰 실제 대화 스크립트, 피드백 (0) | 2023.08.29 |
[우테코] 레벨로그: 레벨3 동안 공부한 내용들을 정리하며 (2) | 2023.08.29 |