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

우아한 테크 캠프 PRO, 7주차 레거시 코드 리팩토링

반응형

우아한 테크 캠프 PRO, 7주차 과제 - 레거시 코드 리팩토링


레거시 코드...

  • 이해할 수 없고 수정하기도 힘든 코드를 지칭
  • 레거시 코드는 모든 개발자가 극복해야 할 난제
  • 왜 시스템은 부패해가는 것일까? 왜 시스템은 깨끗한 상태에 머물러 있지 않을까?란 물음을 갖고 있는 시스템



기존 레거시 코드 문제점 or 레거시로 쌓이는 원인

  • 원인#1: 빈약한 도메인 모델
    • 도메인 모델을 데이터만 가지는 데이터홀더 개념의 단순 오브젝트로 사용
    • 서버사이드 아키텍처라고 제시되는 구조가 빈약한 도메인모델의 사용을 부추기고 있다는 점이 문제
    • 결국 비즈니스 로직이 Service Layer에 거대하게 담기는 Big Service Layer를 야기
  • 원인#2: Big Service Layer
    • 위와 같은 원인으로 생긴 Service Layer는 도메인 로직을 여러 곳에 산재하게 만들 뿐더러 코드의 중복과 오브젝트의 재활용성을 극히 떨어뜨리게 한다.
    • 과거로부터 전해 내려온 초기 스프링 구축 코드를 사용 이유없이 쓰고 있었던 것 같다...



도메인 주도 설계 등장 배경

  • 기존 데이터 베이스 위주의 개발(데이터 베이스 모델링이 중요하지 않다는 게 아님)로 모델링과 객체 지향 개발간의 불일치가 발생
  • 불일치는 불필요한 해석 과정을 거치게 된다.
    • SQL 문법에서 벗어날 수 없었다.
    • 개발 코드와 DB가 종속되게 된다.
  • 이런 불일치를 해소하려고 등장한 것이 도메인 주도 설계
    • 도메인 모델의 적용 범위를 구현까지 확장하여 도메인 지식을 구현 코드에 반영
    • 즉 기존에 Service Layer에 담긴 비즈니스 로직이 각 관심사에 맞게 도메인 모델로 들어가게 된다. (코드 응집력 향상)



그렇다면 도메인이란?

  • 일반적인 요구사항, 전문 용어, 그리고 컴퓨터 프로그래밍 분야에서 문제를 풀기 위해 설계된 어떤 소프트웨어 프로그램에 대한 기능성을 정의하는 연구의 한 영역
  • 설명이 조금 복잡한데 간단히 요약하면 소프트웨어로 해결하고자 하는 문제 영역을 나타낸다.
  • 도메인 모델
    • 특정 도메인을 개념적으로 표현한 것
    • 도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 된다.
      • 개발자만의 언어로 도메인을 작성하면 안되는 이유이다. (참여하는 모든 사람이 이해할 수 있도록 쉽게 작성 되어야 한다)
      • 도메인에서 사용하는 용어를 코드에 반영하지 않으면 그 코드는 개발자에게 코드의 의미를 해석해야 하는 부담을 준다.
      • 코드의 가독성을 높여서 코드를 분석하고 이해하는 시간을 절약한다.



BOUNDED CONTEXT

  • 한개의 슈퍼 모델로 모든 하위 도메인을 표현하기는 힘들다
    • 각 도메인마다 표현하려는 용어도 제 각기이기 때문에
  • 그렇기 때문에 하위 도메인마다 도메인 모델을 생성해야한다
  • 모델은 특정한 컨텍스트안에서 의미를 갖는다
  • 이렇게 구분된 경계를 가진 컨텍스트를 BOUNDED CONTEXT라고 부른다



도메인 주도 설계 기본 요소

  • Value Object (VO)
    • 밸류 타입은 불변
    • 의미를 명확하게 표현하거나 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용
    • 항상 equals() 메서드를 오버라이드할 것을 권고한다 ( + hashCode )
  • Entity
    • 식별자를 갖는다.
      • 식별자는 엔티티 객체마다 고유해서 각 엔티티는 서로 다른 식별자를 갖는다
    • 도메인 모델에 set 메서드 넣지 않기
      • 도메인 모델에 get/set 메서드를 무조건 추가하는 것은 좋지 않은 버릇
      • 특히 set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다
        • set 메서드의 또 다른 문제는 도메인 객체를 생성할 때 완전한 상태가 아닐 수도 있다는 것이다
        • 도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다
        • 생성자에서 최대한 값을 검증해서 빠르게 에러 검출을 할 수 있다
  • Aggregate
    • 관련 객체를 하나로 묶은 군집
    • 애그리거트는 군집에 속한 객체들을 관리하는 루트 엔티티를 갖는다
      • 주문 Aggregate ( 주문(Root Entity) - 주문항목(Child Entity) )
    • 애그리거트끼리 묶인 엔티티들은 유사하거나, 동일한 라이프 싸이클을 갖는다
    • 애그리거트로 묶어서 바라보면 좀 더 상위 수준에서 도메인 모델 간의 관계를 파악할 수 있다
  • Aggregate Root
    • 애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것이다
    • 애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 구현한다
      • 이해한 바로는 애그리거트 밖으로 나가야 할 기능은 애그리거트 루트가 가지고 있다
    • 애그리거트 참조
      • JPA를 사용하여 직접 참조(객체 매핑)을 이용하게 되면 별다른 로직 없이 객체 그래프 탐색을 할 수 있단 장점이 있다
      • 하지만 직접 참조할 때 발생할 수 있는 가장 큰 문제는 편리함을 오용할 수 있다
        • 애그리거트끼리의 경계가 모호해지며, 복잡성이 증가한다.
        • 한 애그리거트에서 다른 애그리거트를 변경하는 일이 발생한다
      • ID를 이용한 참조 방식을 사용하면 복잡도를 낮추는 것과 함께 한 애그리거트에서 다른 애그리거트를 수정하는 문제를 원천적으로 방지할 수 있다
        • "복잡도를 낮춘다"라는 뜻은 서로간의 엃혀있는 의존성 관계를 풀어낸다는 뜻으로 생각된다
        • 순환 의존성 참조라든가 불필요한 의존 관계를 끊어 애그리거트의 독립성을 확보? 한다고 이해했다
  • Repository
    • 구현을 위한 도메인 모델
    • 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의
    • 애그리거트를 구하는 리포지터리 메서드는 완전한 애그리거트를 제공해야 한다
      • 정말 중요한 부분인 것 같다
      • 가끔 모델 한 개로 여러 기능을 저장, 조회를 하게 되는데 각 기능별로 내용이 다르다 보니 사용하지 않거나, 매핑되지 않은 컬럼이 생긴다
      • 다른 개발자가 해당 모델을 사용할 때 결국 테이블의 정보, 실제 쿼리, 매핑된 정보를 확인하게 되는 불편함을 야기 (도메인 객체의 신뢰성이 떨어짐)
        • 이를 잘못하게되면 심지어 NullPointException이 발생할 수 도 있다



도메인 서비스

  • 한 애그리거트에게 넣기 힘든 개념들이 있다
    • 예를 들어 주문, 회원, 배송, 상품 등의 애그리거트가 존재한다고 가정했을때, 실제 결제 담당은 누가 가져가야할까?
    • 이런 애매한 상황에서 애그리거트에 억지로 넣기 보다는 도메인 서비스를 이용
    • 도메인 영역의 애그리거트나 밸류와 같은 다른 구성요소와 비교할 때 다른 점은 상태 없이 로직만 구현한다
      • 기존의 응용 서비스 계층과의 차이는 애그리거트의 상태 값을 변경하거나 계산하는지로 판단하면 된다
  • 객체 참조를 통해서 결합되어있는 것을 한 곳으로 몰아서 객체 참조의 결합도는 낮지만 로직간의 결합도를 높이는 것이 도메인 서비스



도메인 주도 설계의 아키텍쳐

  • 표현 영역
    • MVC 패턴 기준으로 Controller Layer
    • 사용자가 시스템을 사용할 수 있는 (화면) 흐름을 제공하고 제어
    • 사용자가의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다.
  • 응용 서비스 영역
    • 사용자의 요청을 처리하기 위해 리포지터리로부터 도메인 객체를 구하고, 도메인 객체를 사용한다.
    • 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다
      • 레거시 코드를 리팩토링 주된 대상
    • 도메인 객체 간의 실행 흐름을 제어
    • 트랜잭션 처리
    • 도메인 영역에서 발생시킨 이벤트 처리
    • 응용 프로그램 서비스를 설계 할 때 중요한 한 가지 결정은 사용할 데이터와 반환 할 데이터를 결정하는 것
      • 도메인 모델을 직접 사용하거나, - Domain Model Everywhere
        • Open Session In View 주의
          • 뷰 렌더링 시점에 영속성 컨텍스트가 존재하지 않기 때문에 Detached 객체의 프록시를 초기화할 수 없다면 영속성 컨텍스트를 오픈된 채로 뷰 렌더링 시점까지 유지하자는 것 입니다.
          • 즉, 작업 단위를 요청 시작 시점부터 뷰 렌더링 완료 시점까지로 확장하는 것 입니다
      • 별도의 DTO(Data Transfer Object) 사용하기 - Pure Domain Model



의존 역전 원칙 ( DIP )

  • "고수준의 모듈은 저수준의 모듈의 구현에 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야한다"
  • 설명만 들으면 어렵다.. 흔히 말하는 자동차와 타이어의 관계로 다시 설명하자면
    • 겨울이 되어 자동차(고수준 모듈)에 스노우 타이어(저수준 모듈)를 장착하게 설계
      • 즉 고수준 모듈이 저수준 모듈에 의존하고 있습니다
    • 겨울이 지나고 자동차(고수준 모듈)의 타이어를 일반 타이어(저수준 모듈)로 변경하려고 합니다.
      • 저수준 모듈의 변경으로 고수준 모듈에 영향을 준다
      • 이는 개방 폐쇄 원칙에 위배됩니다. ( 객체는 확장에는 열려있어야하고 수정에는 닫혀있어야한다 )
      • 즉 기능을 변경하거나 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않아야 하는데 타이어를 갈아끼우기 위해서는 자동차의 수정이 불가피합니다.
    • 추상화를 이용해 위 문제를 해결할 수 있는데 이를 의존 역전이라고 합니다
      • 자동차가 타이어 실제 객체에 의존하는 것이 아닌 타이어라는 인터페이스에 의존 ( 같은 애그리거트 묶음 )
      • 저수준 모듈이 해당 인터페이스를 구현한 각각의 타이어 객체를 생성
  • 의존성 싸이클을 위의 방식으로 해결할 수 있다
    • 의존성 싸이클은 코드가 변경에 취약(하나를 변경하면 다른 곳에 영향)하고 개발 확장성도 떨어진다



도메인 이벤트

  • 하나의 애그리거트가 변경되었을때 의존하고있는 다른 애그리거트들도 변경이 되게 되는데 결합도가 높으면 변경에 취약해진다
  • 정말 결합도를 최대한 느슨하게 하고싶다
  • A 하나의 이벤트가 끝났을 때 B,C를 실행해야하지만 A와 B,C의 결합도를 느슨하게 만들고 싶을 때 사용한다



Spring ApplicationEvent

  • 위에서 말한 도메인 이벤트을 사용할 수 있는 한가지 방법
  • 이벤트 생성 주체는 AbstractAggregateRoot 상속후 이벤트를 발행(registerEvent)한다
    • 발행되는 시점은 Commit이 될 때
  • 발행된 이벤트는 ApplicationEventPublisher를 통해 ApplicationListener들에게 전달된다.
    • Async를 달아 비동기로 처리가능, 동기도 가능
    • EventListener
    • ApplicationListener( 4.2이후로는 구현안해도됨 )
  • 이벤트 파라미터때문에 다시 의존성 싸이클이 돌 수 있다.
    • 해당 기능을 애그리거트에서 분리, 패키지를 분리하자



피드백

  • 조회만 하는 경우 @Transactional(readOnly = true) 걸어주면 이점이 있다.
    • flush를 생략해서 약간의 성능 향상을 얻을 수 있다. ( 데이터 변경이 없으니 flush 없어도됨 )
    • 단 같은 클래스(프록시)내의 메서드 호출로 @Transactional을 변경할 순 없다.
  • Getter 사용을 지양하고 각 관심사별로 도메인 추출을 해보자
  • API로 들어오는 파라미터는 Spring Validation을 통해 검증할 수 있는 대로 검증하자
    • 불필요한 로직을 줄일 수 있고, 빠른 에러 검출 가능
  • 코드는 위에서 아래로 자연스럽게 읽히도록 하는 것이 가독성에 좋다
    • CleanCode에 따르면 public끼리 위에서부터 작성하고 밑에 private 메서드를 모아두는 것은 아무 의미가 없다
    • 참조되는 메서드로 부터 가장 가깝게 둬라 ( 위에서 아래로 자연스럽게 읽히도록 하라 )
728x90
반응형