본문 바로가기
Education/우아한테크캠프Pro

[우아한테크캠프Pro] 6주차 - 레거시 코드 리팩터링

by 잭피 2021. 1. 31.

 

안녕하세요~ 잭코딩입니다!

 

(해당 업체에서 광고를 받지 않았으며, 교육비를 직접 내고 교육을 받고 있습니다)

교육을 받으며 느낀 점을 적기 위한 포스팅입니다


드디어 마지막 미션을 끝냈습니다!

 

6주차 지하철 노선도 - 레거시 코드 리팩터링 미션 후기입니다

 

마지막 미션은 아래와 같은 Step으로 진행되었습니다

 

Step1 : 테스트를 통한 코드 보호

Step2 : 서비스 리팩터링

Step3 : 양방향을 단방향

Step4 : multi module 적용

 

각 스텝들은 레거시 코드를 리팩터링하는 일련의 과정입니다

지금까지 했던 미션들에서 배웠던 내용을 이 미션에서 모두 적용할 수 있었습니다

 

간단히 각 스텝들을 살펴보겠습니다 

Step1 : 테스트를 통한 코드 보호

Step1 코드리뷰 링크

 

먼저 API의 요구사항을 정리합니다

# Step1 - 테스트를 통한 코드 보호 
## 요구 사항 1
### 상품(/products)
* 상품을 등록할 수 있다
* 상품의 가격이 올바르지 않으면 등록할 수 없다
    * 상품의 가격은 0원 이상이어야 한다 
* 상품의 목록을 조회할 수 있다

### 테이블그룹(/table-groups)
* 테이블 그룹을 등록할 수 있다 
    * 아래와 같은 조건에서 등록이 가능하다
        * 주문 테이블이 비어있지 않고 주문 테이블의 사이즈가 2이상이다
    * 테이블 그룹이 등록되면서 그룹에 들어있는 주문 테이블의 테이블 그룹 id가 등록되고 상태가 비어있지않음으로 변경된다
* 각 주문 테이블의 테이블 그룹 id를 비운다
    * 아래와 같은 조건에서 비울 수 있다
        * OrderStatus가 COOKING, MEAL이 아님

### 테이블(/tables)
* 테이블을 등록할 수 있다
* 테이블 목록을 조회할 수 있다
* 테이블의 상태를 변경할 수 있다
    * 아래와 같은 조건에서 변경이 가능하다
        * 테이블 존재
        * 테이블 그룹 존재하지 않음 
        * OrderStatus가 COOKING, MEAL이 아님
* 테이블의 고객수를 변경할 수 있다
    * 아래와 같은 조건에서 변경이 가능하다
        * 게스트수가 0이상이다
        * orderTableId가 존재해야한다
        * 테이블의 상태가 비어있지 않아야 한다
        
... (이하생략)

그리고 이 요구사항을 검증하는 테스트 코드를 작성해줍니다

 

ex) TableServiceTest

@SpringBootTest
class TableServiceTest {

    @Autowired
    private TableService tableService;

    @MockBean
    private OrderDao orderDao;

    @DisplayName("테이블을 생성한다")
    @Test
    void create() {
        OrderTable orderTable = 테이블을_생성한다(1l, 0, true);

        assertAll(
                () -> assertEquals(orderTable.getNumberOfGuests(), 0),
                () -> assertEquals(orderTable.isEmpty(), true)
        );
    }

    @DisplayName("테이블들을 조회한다")
    @Test
    void list() {
        List<OrderTable> list = tableService.list();

        assertThat(list.size()).isGreaterThanOrEqualTo(1);
    }

    @DisplayName("테이블의 상태를 변경할 수 있다")
    @Test
    void changeEmpty() {
        OrderTable orderTable = 테이블을_생성한다(1l, 0, true);
        orderTable.setEmpty(false);

        when(orderDao.existsByOrderTableIdAndOrderStatusIn(anyLong(), anyList())).thenReturn(false);
        OrderTable changeOrderTable = tableService.changeEmpty(orderTable.getId(), orderTable);

        assertThat(changeOrderTable.isEmpty()).isFalse();
    }

    @DisplayName("테이블의 상태를 변경할 수 없다 : OrderStatus가 COOKING, MEAL인 경우")
    @Test
    void changeEmptyException() {
        OrderTable orderTable = 테이블을_생성한다(1l, 0, true);
        orderTable.setEmpty(false);

        when(orderDao.existsByOrderTableIdAndOrderStatusIn(anyLong(), anyList())).thenReturn(true);

        assertThatThrownBy(() -> tableService.changeEmpty(orderTable.getId(), orderTable))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @DisplayName("테이블의 고객수를 변경할 수 있다")
    @Test
    void changeNumberOfGuests() {
        OrderTable orderTable = 테이블을_생성한다(1l, 0, false);
        orderTable.setNumberOfGuests(10);
        OrderTable changeOrderTable = tableService.changeNumberOfGuests(orderTable.getId(), orderTable);

        assertThat(changeOrderTable.getNumberOfGuests()).isEqualTo(orderTable.getNumberOfGuests());
    }

    @DisplayName("테이블의 고객수를 변경할 수 없다 : 게스트수가 0미만인 경우")
    @Test
    void changeNumberOfGuestsNumberException() {
        OrderTable orderTable = 테이블을_생성한다(1l, 0, false);
        orderTable.setNumberOfGuests(-1);

        assertThatThrownBy(() -> tableService.changeNumberOfGuests(orderTable.getId(), orderTable))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @DisplayName("테이블의 고객수를 변경할 수 없다 : 테이블 상태가 비어있는 경우")
    @Test
    void changeNumberOfGuestsStatusException() {
        OrderTable orderTable = 테이블을_생성한다(1l, 0, true);
        orderTable.setNumberOfGuests(10);

        assertThatThrownBy(() -> tableService.changeNumberOfGuests(orderTable.getId(), orderTable))
                .isInstanceOf(IllegalArgumentException.class);
    }

    private OrderTable 테이블을_생성한다(Long id, int numberOfGuest, boolean empty) {
        return tableService.create(new OrderTable(id, numberOfGuest, empty));
    }
}

이렇게 먼저 레거시 코드의 비지니스 로직을 검증하는 테스트를 작성한 후에, 리팩터링을 시작합니다

 

Step2 : 서비스 리팩터링

Step2 코드리뷰 링크

 

Repository Layer에서 JDBC -> JPA로 변경이 필요했습니다

JPA를 사용하기 위해 먼저 테이블 구조를 파악하려고 테이블 관계를 그렸습니다

그리고 이 테이블을 보면서 domain과 repository을 설계하였습니다

 

테이블 관계

JPA로 리팩터링 후 서비스에서 기존 JDBC dao를 바라보는 부분의 의존성을 끊고 JPA repository의 의존성을 추가합니다

그리고 Step1에서 만든 테스트들을 돌려보면 와장창 깨지는데, 다시 비지니스 로직을 검증하면서 테스트 코드를 수정합니다

JPA로 리팩터링 후 기존 작성한 모든 테스트 케이스가 성공하면 기존 JDBC 레거시 코드를 삭제합니다 

 

이제 비지니스 로직을 서비스에서 도메인으로 이동시켜줍니다

 

우아한테크캠프Pro 교육자료 

실제 TDD, OOP를 적용하려면 핵심 비지니스 로직은 도메인 객체가 담당하도록 구현하는게 좋습니다

테스트 하기 쉬운 부분과 어려운 부분을 분리하여 쉬운 부분은 단위 테스트로 구현하고 지속적인 리팩터링을 수행할 수 있습니다

 

따라서 기존에 있는 서비스 로직을 각 도메인 책임에 맞게 이동시켜주면서 리팩터링을 진행합니다

그리고 기능을 검증해보고 싶다면 기존에 작성해둔 테스트 코드를 한번 돌려보면 됩니다 

(이 때, 기존에 작성해둔 테스트 코드가 빛을 발합니다)

 

이렇게 비지니스 로직을 서비스에서 도메인 책임에 맞게 리팩터링을 진행하면 Step3로 넘어갑니다

 

Step3 : 양방향을 단방향

Step3 코드리뷰 링크

 

rdb 관점에서 보면 테이블 간 관계는 무조건 양방향으로 되어있는데,

객체지향적으로 본다면 모두 양방향은 아니고, 특정 객체는 단방향으로만 참조해도 됩니다

DDD의 개념으로 Aggregate Domain을 나누고 단방향을 설정하는 스텝이었습니다 

 

먼저 현재 JPA 도메인 간의 관계를 그려보았고, 총 3개의 양방향 참조가 있었습니다

모두 양방향이 될 필요가 없다고 판단하여 양방향을 끊고 단방향으로 변경하였습니다

위의 그림 -> 아래의 그림으로 변경을 하였습니다

이 부분을 수정하면서도 테스트 코드의 검증을 통해 확신을 가지고 변경할 수 있었습니다

초반에 만들어둔 테스트 코드가 든든한 힘이 되어주었습니다

 

코드리뷰를 받고 table_group -> order_table 단방향을 양방향으로 수정했습니다

그 이유는 create-update할 때 양방향으로 매핑할 경우 더 관리하기 좋다고 판단했습니다

 

단방향일 경우 외래키에 null이 들어갈 수 있어서 따로 update를 다시 수행해줘야 했습니다

하지만, 양방향으로 구성할 경우 jpa가 insert와 update를 적절히 수행해주므로 따로 update를 수행하지 않아도 됩니다

 

Step4 : multi module 적용

Step4 코드리뷰 링크

 

마지막은 Gradle의 멀티 모듈 개념을 적용해 서로 다른 프로젝트로 분리해보는 스텝이었습니다

계층 간의 독립된 모듈을 만들 수 있었습니다

글쓴이는 module-api, module-common, module-damain으로 분리하여 최종 스텝을 수행했습니다

 

글쓴이는 마지막 미션을 진행하면서 정말 많은 도움이 되었습니다

레거시 코드가 있다면 이런 방식으로 더 자신있게 리팩터링을 할 수 있다는 생각이 드네요

가장 오래걸렸고 어려웠던 미션이라서 다른 미션들에 비해 더 자세히 리뷰해보았습니다


7-8주차에서는 AWS & 도커 & MySQL 튜닝 등 수업과 선택 미션이 진행되었습니다

(필수 미션을 수행하느라 수업만 듣고 선택 미션을 해보지는 못했습니다)

 

글쓴이는 미션 마감일 딱 하루 전에 모든 미션을 완료하고 수료할 수 있었습니다!

 

 

이제 마지막으로 수료자 발표 및 우수 수료자 선정을 끝으로 우아한테크캠프 Pro 교육과정이 끝나게 됩니다

9주동안 즐겁게 코딩하고 많이 배울 수 있어서 좋았습니다

막상 끝난다고 생각하니 조금 아쉽네요..

 

그럼 수료자 발표가 끝나고 마지막 리뷰를 작성하러 오겠습니다!

 

 

댓글