굴러가는 자동차에 안전하게 타이어 교체하기(w. CMS 기능 개발)

Oct.17.2023 김정규

Backend Programming General QA/Testing

📃 배경

배민외식업광장서비스팀은 외식업 사장님에게 도움이 되는 콘텐츠와 기능을 제공하는 ‘배민외식업광장’ 포털사이트를 개발하고 있습니다. 더하여, 배민외식업광장 포털사이트에는 콘텐츠를 노출할 영역을 관리할 수 있는 서비스를 가지고 있습니다.

이 글에서는 배민외식업광장 사이트에서 노출중인 콘텐츠에 부작용 없이 안전하게 특정 기능을 개발했던 경험을 공유하고자 합니다.

특정 영역에 어떤 콘텐츠를 노출할 것인지 관리하는 서비스를 CMS(Content Managment System)라고 부르며, 이해관계자 분들과 협업하여 CMS를 활용해 외식업사장님들에게 유용한 정보를 어떻게 잘 전달할 수 있을지 고민합니다. 아래 그림은 배민외식업광장 포털사이트의 메인 화면입니다.


메인 화면

그림 1 – 배민외식업광장 메인 화면


배민외식업광장서비스팀의 경림님이 작성해주신 콘텐츠 브로드캐스팅이 가능한 CMS 만들기: 배민외식업광장 슬롯화를 통해 CMS에 대한 자세한 내용을 확인하실 수 있습니다.


그림 1 - 배민외식업광장 메인 화면에서 사용되는 도메인 용어에 대해서 간략히 말씀드리면, 그림 1 내에 있는 민트색 박스 하나를 저희는 콘텐츠슬롯(ContentSlot) 이라 부릅니다. 이때 콘텐츠슬롯(ContentSlot)의 정보를 갖고 노출될 특정 영역에 대한 데이터를 갖고 있는 객체를 콘텐츠슬롯스코프(ContentSlotScope)라고 부릅니다.

연관관계 표시

그림 2 – ContentSlot과 ContentSlotScope의 관계


여기서 제가 구현해야만 하는 기능은 여러 종류의 콘텐츠슬롯스코프(ContentSlotScope) 정보가 담긴 Preset을 저장할 수 있는 구조, 아래 그림 3 - Items의 Preset 설정하기와같이 유사한 기능을 만드는 것을 목표로 합니다.


그림 3 - Items의 Preset 설정하기

그림 3 – Items의 Preset 설정하기


위 그림은 iTerm라는 터미널에서 제공하는 기능으로 컬러별 Preset을 미리 만들어 놓고, 필요에 따라 Preset을 변경하여 외형을 변경할 수 있습니다.

그림 3 - CMS 관리화면

그림 4 – CMS 관리화면


그림 4 - CMS 관리화면는 CMS 관리 페이지 내에 컨텐츠 노출 순서와 어떤 콘텐츠슬롯스코프(ContentSlotScope)가 있는지 보여주는 화면입니다.

다시 배민외식업광장 메인 화면으로 돌아오면, 현재는 그림 4 - CMS 관리화면과 같이 단 하나의 콘텐츠 레이아웃 구조로 운영중에 있습니다. 그러므로 콘텐츠슬롯스코프(ContentSlotScope) 데이터가 변경된다면 그림 1 - 배민외식업광장 메인 화면에 즉각 반영되는 시스템을 가지고 있습니다.

결과적으로 목표하는 바는 즉각 반영되는 시스템이 아닌, 미리 사전에 여러 종류의 콘텐츠 레이아웃을 만들어 놓고, 추후 상황에 맞춰 사용할 수 있도록 하는 것입니다.

🐾 개발 과정

가장 먼저 해야 될 것은? 비즈니스 로직 코드에서 사용된 도메인 용어 이해하기

프로젝트를 진행하며 난관에 봉착한 첫번째는 믿고 살펴볼 수 있는 관리되는 테스트 코드가 없다는 점입니다. 테스트 코드가 전달하는 가치 중 프로덕트 코드를 사용하는 첫 번째 클라이언트로서 도메인 사용 설명서 역할을 할 수 있다고 생각합니다. 그러므로, 테스트 코드가 없는 것은 프로덕트 코드를 이해하기 어렵게 만드는 요인 중 하나였습니다.

프로덕트 코드만 보고 비즈니스 로직을 이해해야 한다면 할 수 있지만, 이해한 바가 120% 맞는다고 확신하기 어렵습니다. 이유는 정확히 알지 못하는 도메인 용어와 두껍게 작성된 애플리케이션 서비스 계층과 컴포넌트 간의 의존성에 대한 이해가 쉽지 않기 때문입니다.


하나의 서비스에 여러 의존성이 묶여있는 코드

그림 5 – 하나의 서비스에 여러 의존성이 묶여있는 코드


이런 상황에서 제가 만들어야만 하는 기능을 구현할 수 있을지 미지수였습니다. 그래서 쉽고 빠르게 코드를 이해해 볼 수 있는 수단으로 비즈니스 로직에 사용된 도메인 용어 간의 관계를 파악하여 이것을 도식화하는 것이였습니다.

그림 6 - 개발하기 전에 시도해 본 도메인 관계 도식화

그림 6 – 개발하기 전에 시도해 본 도메인 관계 도식화


그림 7 - 각 도메인 단어별 화면 간단 매핑

그림 7 – 각 도메인 단어별 화면 간단 매핑


직접 빈 화면에 사용된 도메인 용어를 활용해 그림 6 - 개발하기 전에 시도해 본 도메인 관계 도식화를 팀원들에게 공유하며 제가 이해한 바가 맞는지, 확인했습니다. 이런 과정을 거치게 되니 비즈니스 로직에 사용된 도메인의 단어가 친숙해지기 시작했고, 직접 각각의 도메인 용어에 대한 정의를 누구에게나 설명할 수 있었습니다.

JPA가 기억나지 않아요🥺

프로덕트 소스 코드 정보를 바탕으로 도식화했지만, 이렇게 했음에도 이해되지 않는 것 있었습니다. 저는 우아한형제들에 올해 2월에 입사하기 전까지 약 1.5년 동안은 JPA을 사용하지 않은 탓에 동작 원리에 대해서 익숙할 정도의 숙련도를 갖고 있지 않았습니다. 예를들어 @OneToMany 의 Annotation에 붙어있는 mappedBy, joinColumn, persistance 시점, 양방향 설정시 편의 메소드 설정 등등이 있었습니다. JPA 연관관계를 설정한 뒤에는 영속성 코드가 과연 제가 의도하는 대로 동작할 것인지에 대한 의문이 남았습니다. 동시에, DB Table의 제약조건과 JPA Entity가 일치하는지 여부에 대한 확인이 필요했습니다.

또한, 앞서 언급 드린 것과 같이 프로젝트 내에서는 관리되는 테스트 코드가 없었습니다. 그러므로 영속성 계층이 의도한 바에 따라 잘 동작한다는 것을 보장받기 위해 테스트 코드 작성을 시도했습니다. 누군가는 간단하게 테스트 코드 그냥 작성하면 되는 거 아니야? 라고 말할 수 있을 수 있지만, 아래 코드와 같이 작성해야만 영속성 계층을 테스트할 수 있는 기반을 만들 수 있었습니다.


@DataJpaTest(properties = {"transactionManager=jpaTransactionManager"})
@ContextConfiguration(classes = {JpaRepositoryConfiguration.class, QuerydslConfiguration.class, JpaConfig.class})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(properties = {"spring.config.location=classpath:content.yml,classpath:content-test.yml"})
class ContentRepositoryTest {

    @Autowired
    ContentRepository sut;

    .. 생략...
}

아무런 동작을 하지 않는 테스트가 정상적으로 동작하기 위해 점진적인 방식의 코드 추가를 시도했습니다. 특히 @ContextConfiguration의 경우 20만 라인의 전체 코드에 맺어진 Configuration을 모두 알 수 없기 때문에 Config 을 하나씩 넣어보고 Run 한 뒤에 발생하는 에러 로그를 보며 하나씩 Configuration을 추가하여 최종적으로 아무런 동작을 하지 않는 영속성 테스트가 가능한 코드를 만들었습니다.

위 테스크 코드를 기반으로 필요했던 테스트를 진행해보며 부족했던 JPA 이해도를 높였습니다.

어떻게 요구사항을 해결할 할 수 있을까?

다시 프로덕트 소스 코드로 돌아와 전체적으로 구현된 아키텍처가 전형적인 layered 아키텍처 패턴으로 구성된 것을 확인하고, 다시 그림 6 개발하기 전에 시도해 본 도메인 관계 도식화로 돌아가 어떻게 개발하면 좋을지 고민했습니다.

결론부터 말씀드리면 최종적으로 만들어진 도식화는 다음과 같습니다.

그림 8 – 최종 결정된 도메인 모델링


요구사항이 ‘미리 설정된 여러벌의 컨텐츠슬롯판(ContentSlotPlate)을 사용할 수 있다.’ 이므로 처음 접근한 방식은 ContentSlotManagement 에 1대N로 연결된 ContentSlotScope 관계를 끊어내고 그 중간에 컨텐츠슬롯판(ContentSlotPlate)을 추가한다면 되지 않을까? 라는 생각으로 도메인모델링을 그려봤었습니다.


그림 9 – 문제 해결을 위해 처음 시도했던 도메인 모델링


그러나 그림 9 - 문제 해결을 위해 처음 시도했던 도메인 모델링과 같이 도메인 모델링 과정에서 도메인에 대한 요구사항을 이해했다고 하더라도, 도메인 모델링을 코드로 옮겼을 때, 의도하는 바가 명확히 동작하지 않을 수 있었습니다. 왜냐하면, 그림 9 - 문제 해결을 위해 처음 시도했던 도메인 모델링을 개발 중간 과정에서 팀원분들에게 요구사항과 함께 공유했을 때, 팀원분들이 주신 피드백을 통해 제가 인지하지 못했던 요구사항을 발견할 수 있었기 때문입니다.

콘텐츠슬롯판(ContentSlotPlate) 각각이 가지는 Version에 대해서 관리되어져야 만 하는데 그 요구사항을 놓쳤습니다. 다행히 코드가 많이 작성된 상황이 아니었기 때문에 팀원들의 피드백을 받고 도메인모델링의 코드를 빠르게 수정할 수 있었습니다.

🛞 굴러가는 자동차에 안전하게 타이어 교체하기

그림 8 - 최종 결정된 도메인 모델링을 그대로 코드로 옮겼고, 이를 바탕으로 구현과 테스트까지 마치며 어느정도 구현을 완료될 수 있었습니다.

하지만 이미 동작 중인 서비스에 객체 연관관계를 끊고, 그사이에 어떤 연관관계를 추가하기란 쉽지 않았습니다. 혹시라도 콘텐츠 노출이 되지 않는 상황이 발생할 수도 있기 때문이다. 그래서 어떻게 안전하게 새롭게 추가된 기능을 이식시킬 수 있을지 고민되었습니다.

무엇보다 한 번에 문제를 해결할 수 없다고 판단했습니다. 이 문제를 해결하기 위해서는 크게 3가지 단계를 통해 해결할 수 있을것이라 생각했습니다.

  1. Query API 가 요청되어지면 IF문 분기(새롭게 만들어진 연관관계의 객체의 데이터 존재여부 확인)를 통해 임시방편으로 기존 코드가 동작될 수 있도록 한다.

  2. 기존 코드가 동작되고 있는 상황에서 Command API 요청하면 새롭게 만들어진 연관관계의 초기데이터를 생성한다. 이 후에 Query API 요청이 들어오면, 1번에서 만들었던 IF문 분기는 새롭게 추가된 연관관계로부터 데이터를 조회하게 됩니다.

  3. 1단계에서 만든 IF문 분기에서 더 이상 유효하지 않은 기존 코드를 삭제하여, Query 요청시 새롭게 추가된 연관관계를 통해 데이터가 안전하게 조회됨을 확인한다.

3가지 단계 작업하는 것을 고민했고, 무엇보다 핵심은 Command와 Query 을 분리해서 동작시킴으로써 동작하고 있는 서비스에는 영향이 가지 않도록 하는 것이였습니다.

1단계의 경우 다음 같은 코드를 삽입하여 임시방편을 기존코드가 조회될 수 있도록 유도했습니다.

@Transactional(readOnly = true)
public ContentSlotManagementDto find...(String displayScopeType) {
    ...

    //TODO 저장후 추후 변경되어야 한다.
    if (contentSlotManagement.getContentSlotPlates().isEmpty() == false) {
       // 중간에 ContentSlotPlate 가 새롭게 만들어지면 필요해진 코드
        ....
        return ...
    } else {
        // 기존에 동작하던 코드 그대로
        return ...
    }
}

//TODO 로 표시함으로써 다음 3단계 작업시 인지할 수 있도록 하고 마크표시를 했습니다.

2단계에서는 먼저 Command 관점에서 도메인 모델링 변경에 따른 JPA의 연관관계가 변경될 것이기 때문에 그림 4 - CMS 관리화면에서 수정하기 버튼을 누르게 되면, 기존에는 ContentSlotScope 에 ContentSlotManagement의 정보만 포함해 데이터를 저장하는 형태였다면, 변경된 객체 연관관계에 따라 저장하기 버튼을 누르면, 컨텐츠슬롯판(ContentSlotPlate)을 생성하고, 그 슬롯판의 첫번째 콘텐츠슬롯버전(ContentSlotPlateVersion)을 생성 한 뒤에, 생성된 Version 정보를 콘텐츠슬롯스코프(ContentSlotScope) 에 담아 저장하게 됩니다.

2단계 이후에 Query 요청이 들어온다면, 1단계에서 만든 IF 문 분기 코드에 따라 새롭게 추가된 연관관계의 데이터를 조회하게 됩니다.

2단계까지 작업 후 릴리스한 뒤에 3단계 작업을 수행합니다. 1단계에서 만들었던 IF 문 코드에서 기존에 동작하던 코드는 삭제합니다.

이렇게 함으로써 최종적으로 "미리 사전에 여러 종류의 콘텐츠 레이아웃을 만들어 놓고, 추후 조건에 맞춰 활용할 수 있도록 하는 기능" 을 운영중인 배민외식업광장 사이트에 안전하게 기능을 이식시킬 수 있었습니다.

아직 끝나지 않았습니다.

이 기능은 아직 저희에게 작은 시작일 뿐입니다. 배민외식업광장서비스팀은 추후 배민외식업광장에 접속하는 모든 배민 사장님이 장사가 더 잘 될 수 있도록 돕는 여정에 적극적으로 함께할 것 입니다.

마지막으로 사장님비즈니스콘텐츠실의 지현님이 작성해주신 배민외식업광장에서는 뭐해요?로 배민외식업광장에 관한 소개로 글을 마치려 합니다.