[설문이용] 설문 클래스를 어떻게 설계해야할까? (feat. DDD, TDD)

정훈희·2024년 9월 5일
6

설문이용

목록 보기
1/2
post-thumbnail

해당 글은 소프트웨어 마에스트로 15기 과정에서 진행 중인 대학생을 위한 설문조사 서비스 "설문이용"을 개발하며 겪은 경험을 정리한 것입니다.

0️⃣ 배경 - 설문 클래스를 어떻게 설계해야할까?

이전에 프로젝트를 진행했을 때는 개발 기간이 짧아 빠르게 기능을 개발하는 것에 중점을 두었다. 그러다 보니 기능이 추가될 수록 예상치 못한 버그가 자주 발생하게 되었다.

이번에 진행하게된 설문이용은 6개월 동안 개발을 하게되었고, 개발이 끝난 후에도 지속적으로 운영 및 유지보수를 하는 것을 목표로 하고있다.

그래서 우리 팀은 개발 비용이 조금 더 들더라도 안정적이고 유지보수가 편한 설계를 할 수 있는 방법을 찾게 되었다.

1️⃣ 안정적이고 유지보수가 편한 설계를 위한 방법 - DDD, TDD

ℹ️ 설계 & 구현 계획 - 설계(DDD), 구현 및 개선(TDD)

우리 팀은 개발 비용이 조금 더 들더라도 유지보수가 편하고, 안정적인 설계를 할 수 있는 방법을 찾기 위해 다양한 개발 방법론을 찾아보고, 멘토님들께도 조언을 구했다.

그 결과, 우리 팀은 개발 비용이 높아지지만 복잡성을 줄이고 유지보수가 편한 구조를 설계할 수 있는 도메인 주도 개발(DDD) 방식을 도입하기로 결정했다.

또한, 개발 비용이 높아지지만 안정성을 올리고 추후 리팩토링에 유리한 테스트 주도 개발(TDD) 방식을 도입하기로 결정했다.

상세한 과정은 아래와 같다.

  1. DDD의 주요 개념들을 적용하여 설문 관련 도메인 클래스들을 설계한다.
  2. 요구사항에 맞는 테스트 코드를 작성한다.
  3. 테스트 코드를 만족하도록 클래스를 구현한다.
  4. 지속적인 리팩토링을 통해 코드 품질을 개선한다.

ℹ️ 구현 결과

image

구현 과정이 담긴 PR 링크

2️⃣ 설문 도메인 설계 과정 및 성과 (feat. DDD)

ℹ️ 설문 도메인 설명

설문 구조

설계할 설문 도메인을 살펴보면, 하나의 설문에는 여러개의 섹션이, 하나의 섹션에는 여러개의 질문이 있는 형태이다.

image

또한, 사용자는 설문에 참여해서 응답을 제출할 수 있고, 위와 같이 응답에 따라 다른 섹션으로 이동할 수 있도록 라우팅 설정 기능을 제공한다.

ℹ️ 어떻게 DDD 방식을 설문에 적용했는가?

image

설문 도메인의 요구사항들을 바탕으로 위와 같이 설문 도메인 클래스들을 설계하였다.

DDD의 주요 개념 중 하나인 Aggregate를 도입하여, 설문과 관련된 객체를 하나의 설문 Aggregate로 묶었다.

또한, 설문 Aggregate root를 Survey로 설정하여 하위 객체에 직접 접근하지 못하도록 하여 하위 객체를 캡슐화하고, 예상치 못한 변경을 막았다.

data class Survey(
    val id: UUID,
    val title: String,
    val sections: List<Section>,
    // 그 외의 속성들
) {
		// Survey Class 생성 시 유효성 검증 진행
    init {
        require(sections.isNotEmpty()) { throw InvalidSurveyException() }
        require(isSectionsUnique()) { throw InvalidSurveyException() }
        require(isSurveyStatusValid()) { throw InvalidSurveyException() }
        require(isFinishedAtAfterPublishedAt()) { throw InvalidSurveyException() }
        require(isSectionIdsValid()) { throw InvalidSurveyException() }
    }
    
    /** 설문의 응답 순서가 유효한지, 응답이 각 섹션에 유효한지 확인하는 메서드 */
    fun validateResponse(surveyResponse: SurveyResponse) { /* 메서드 코드 */ }
    
    fun updateContent(/* 매개 변수들 */): Survey { /* 메서드 코드 */ }
    
    fun start(): Survey { /* 메서드 코드 */ }
    
    fun finish(): Survey { /* 메서드 코드 */ }
    
    // 그 외의 메서드들
}

Survey 클래스의 코드는 위와 같다. 특징은 아래와 같다.

  1. Survey 클래스 생성 시 유효성 검증을 진행하여, 항상 유효한 Survey 클래스만 존재할 수 있도록 했다.
  2. 설문과 관련된 기능을 전부 Survey에 모아두어 응집도가 상승하고, 재사용성이 향상되었다.
  3. 불변 객체로 설계하여, 의도치 않은 객체의 변경을 막아 코드의 안정성이 향상되었다.

ℹ️ 설계 과정 성과 1 - 응집도 상승으로 인한 Service 계층의 복잡도 감소 & 유지 보수성 향상

설문이용에서 설문이 끝나는 경우는 1. 추첨권이 소진되거나, 2. 설문 마감일이 되는 경우가 있다.

두 경우 다 동일하게 설문을 종료 상태로 업데이트 하는 기능이 필요하다.

이러한 기능을 여러 Service 계층에 각자 구현하게되면 아래와 같다.

image

하지만 위의 경우, 설문 종료 로직을 수정하는 경우, 각 서비스의 설문 종료 코드를 수정해야한다. 즉 수정 범위가 증가하게 되어 유지 보수가 어려워진다.

하지만, 설문 종료 메서드를 Survey 클래스에 구현하고, 각 Service 계층에서는 Survey 클래스의 메서드를 사용한다면 어떨까? 이를 그림으로 표현하면 아래와 같다.

image

위의 경우는 Service 계층에선 Survey 클래스의 메서드를 호출하기만 하면 되므로 Service 계층의 복잡도가 감소한다.

또한 설문 종료 로직을 수정하기 위해선 Survey 클래스의 finish 메서드만 수정하면 되고, 추후 설문 종료 기능이 필요한 곳이 생기면 Survey 클래스의 finish 메서드를 재사용하면 되므로 유지 보수성이 향상된다.

ℹ️ 설계 과정 성과 2 - Aggregate root를 설정하여 설문 전체의 일관성을 유지

image

설문이용에서는 위와 같이 선택지에 따라 다른 섹션으로 이동하도록 설정할 수 있다.

이러한 상황에서, 선택지에 따라 이동할 섹션을 존재하지 않는 섹션으로 수정한다면 어떻게 될까?

  • DDD 미적용(라우팅 설정을 직접 수정 가능)
    • 설문 클래스를 거치지 않고 라우팅 설정을 직접 수정할 수 있으므로 존재하지 않는 섹션으로 수정 가능 → 설문 전체의 일관성이 깨짐
  • DDD 적용(Aggregate root를 통해서만 하위 객체 접근 가능)
    • 설문 클래스와 섹션 클래스를 거치면서 변경하려는 섹션이 존재하는지 확인 가능하므로 존재하지 않는 섹션으로 수정이 불가능 → 설문 전체의 일관성이 유지됨

3️⃣ 설문 도메인 구현 과정 및 성과 (feat. TDD)

ℹ️ 어떻게 TDD 방식을 설문에 적용했는가?

image

우리는 정리한 설문에 대한 요구사항을 바탕으로 위와 같이 테스트 코드를 작성했다.

그리고, 테스트 코드가 통과하도록 최대한 빠르게 클래스의 메서드를 구현했다.

image

이 과정에서, 테스트 코드 작성을 놓친 부분이 있는지 확인하기 위해 Jacoco를 통해 테스트 커버리지를 측정하였고, domain 패키지의 테스트 커버리지를 100%로 달성하였다.

이후에는 테스트 커버리지를 100% 유지하면서 지속적으로 리팩토링을 진행하였다.

ℹ️ TDD 적용 성과 1 - 테스트 코드 작성으로 인한 기능 안정성 향상

domain 패키지의 테스트 커버리지를 100% 달성하여, 도메인 클래스의 메서드를 활용하는 서비스 코드의 안정성도 같이 향상되었다.

실제로 테스트 코드 작성 이후, 핵심 도메인 로직에 대한 버그 발생 빈도가 크게 줄었다.

ℹ️ TDD 적용 성과 2 - 리팩토링의 안정성 및 생산성 향상

리팩토링을 진행할 때 아무리 변경을 해도 테스트 코드를 통해 기존과 같은 동작을 한다는 것을 확인할 수 있어서 안정적이고 더 효율적으로 리팩토링을 진행할 수 있었다.

4️⃣ 리팩토링 과정 및 성과

앞서서 설문 도메인을 설계하고, 구현 까지 완료했지만 아래와 같은 이유로 리팩토링을 진행하기로 결정했다.

  1. 설문 제작 시 어려움
  2. 질문 추가에 대한 확장성 부족
  3. 코드 로직의 복잡함(null을 로직에 활용, 라우팅 방식에 대한 모호함, 가독성이 부족한 코드)

리팩토링 과정이 담긴 PR 링크

ℹ️ 리팩토링 결과

image

리팩토링 과정이 담긴 PR 링크

리팩토링 후 설문 클래스 구조 설명 링크

ℹ️ 리팩토링 성과 1 - 질문 인터페이스 세분화를 통한 확장성 증가

image

기존에는 질문 인터페이스 하나와 질문 유형별 구현체의 구조였다.

하지만, 객관식 질문들의 공통 요구사항과 객관식 질문 중 선택지 기반 라우팅의 대상이 될 수 있는 단일 선택 질문에 대한 요구사항이 있는 등 추후 질문 확장 시 코드 중복이 우려되어 세분화를 진행했다.

이를 통해, 앞으로 주관식을 추가하면 TextQuestion 인터페이스 구현, 객관식을 추가하면 단일 선택이면 SingleChoiceQuestion, 다중 선택이면 MultipleChoiceQuestion 인터페이스를 구현하면 되므로 질문 유형 추가에 대한 확장성이 증가했다.

ℹ️ 리팩토링 성과 2 - sealed 클래스를 활용하여 가독성 및 유지보수성 향상

image

기존에는 기타 선택지를 null로 구분하였는데, 이 때 null의 의미가 모호하여 코드의 가독성이 떨어졌었다.

이제 sealed 클래스를 활용하여 명시적으로 기타 선택지임을 알 수 있어 더 유지보수와 가독성이 좋아졌다.

image

섹션 ID도 마찬가지로, 기존에는 마지막 섹션 ID를 null로 구분하여 의미가 모호해서 코드의 가독성이 떨어졌었다.

이제 sealed 클래스를 활용하여 명시적으로 마지막 섹션 ID임을 알 수 있어 더 유지보수와 가독성이 좋아졌다.

5️⃣ 결론

빠르게 기능을 개발하는 것에 집중했던 이전 프로젝트들과 다르게 우리의 목표에 맞는 다양한 개발 방법론을 도입하여 설계 & 개발을 진행하니까 다양한 장점을 겪을 수 있었다.

도메인 주도 개발을 통해선 좋은 설계가 미래의 개발에 얼마나 큰 영향을 주는지 깨달을 수 있었고, 테스트 주도 개발을 통해선 테스트 코드의 든든함을 깨달을 수 있었다.

하지만, 체계적인 설계를 위해 초반에 많은 시간이 소요되었고, 요구사항이 변경되는 경우 테스트 코드 또한 변경해야 해서 개발 비용이 증가할 수 있는 등 단점에 대해서도 겪을 수 있었다.

DDD나 TDD를 무조건적으로 적용하기 보다는, 프로젝트의 목표를 잘 고려해서 적용해야겠다는 생각이 들었다.

profile
DB를 사랑하는 백엔드 개발자입니다. 열심히 공부하고 열심히 기록합니다.

0개의 댓글