ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 객체지향적 리팩토링 맛보기
    Java & Kotlin 2022. 7. 3. 01:40
    반응형

    1) 서론

    개발자에게 리팩토링은 피할 수 없는 순간일 것 같습니다. 개인적으로는 리팩토링은 숨 쉬듯이, 업무와 무관하게 이루어져야 하는 것 같기도 한다는 생각이 들기도 하는데요.

    당시에는 빠르게 개발하는 것이 목적이었기 때문에 코드에 신경을 쓰지 못할 수도 있습니다. 혹은 기능 추가가 계속해서 이루어지면서, 어쩔 수 없이 정리가 필요한 순간이 올 수도 있을 것 같습니다.

    이번 글에서는 임시적으로 추가한 기능이 어느 순간 중요한 기능이 되고, 지속적으로 로직 추가가 발생했던 기능을 리팩토링 한 경험을 공유드립니다.


    2) 어떤 내용을 리팩토링 하나요?

    (모든 내용은 블로그를 위해 가정한 것들입니다. 회사와 무관합니다)

    아래의 상황이라고 가정합니다.

    • A, B 애플리케이션이 각각 다른 서버에 존재
    • A에서 생성된 이미지 파일들은 로컬 디렉터리에 저장 중
    • B에서 생성된 이미지 파일들은 AWS S3에 저장 중
      • B-1, B-2 등 여러 타입의 이미지 파일들 존재
    • A에서 A, B 이미지 모두를 하나의 .zip 파일로 다운로드 기능을 제공해야 함


    기존에는 A 서버의 로컬에 저장된 이미지들만 다운로드하는 기능이었습니다. 그리고 굉장히 빠른 시간 내에 제공되어야 하는 기능이었기 때문에 코드 작성에 많은 고민을 하지 못 했는데요.

    하지만 B 이미지 파일들도 동일한 .zip 파일에 포함되어 제공해야 할 필요가 생겼습니다.

    추가되는 이미지 파일의 목록과 경로가 늘어나면서, 코드는 걷잡을 수 없이 지저분해지게 되었습니다. 하나의 클래스에 하나의 거대한 비즈니스 로직이 탄생하게 되었고, 주석 없이는 코드를 이해할 수 없는 순간까지 오게 되었습니다.

    그래서 위의 코드를 리팩토링 하기로 결정했습니다.


    3) 기존의 잘못된 리팩토링 시도

    최초 리팩토링을 진행할 때는 객체지향에 대해서 이해가 부족했었습니다. 자바를 사용하면서도 객체가 무엇이고, 객체 간의 역할과 책임은 어떻게 나뉘는 것인지 대해 깊게 생각하지 않고 진행했었는데요.

    당시에는 단순히 긴 로직을 함수 단위로 분리하는 것에만 집중했습니다.

     

    장황한 주석으로 설명을 해야 하는 복잡한 로직은 함수로 분리하고, 함수명을 통해 기능을 설명했습니다. 그리고 parameter로 무엇을 넣으면, return type으로 어떤 것을 제공할지 설명했습니다.

    하지만 여전히 하나의 서비스 클래스 모든 것이 존재했고, 엄격한 기능 단위의 구분이 아닌 상황(?)에 따른 구분이었습니다.

    대략적인 예시는 아래와 같습니다.

    이미지 종류에 따른 S3 key 생성 함수들

    • B-1의 key 생성 함수
    • B-2의 key 생성 함수

    이미지들을 메모리로 읽어 오는 함수들

    • A 이미지들을 가지고 오는 함수
    • B 이미지들을 가지고 오는 함수
      • B-1, B-2 각각 존재

    Empty 여부 확인 함수들

    • A 이미지들 empty 여부 확인 함수
    • B 이미지들 empty 여부 확인 함수

    Stream 변환하는 함수들

    • A 이미지들을 OutputStream으로 변환하는 함수
    • B 이미지들을 OutputStream으로 변환하는 함수


    무언가 분리를 많이 한것 같습니다. 하지만 여전히 보기 좋지 않습니다.

    아래의 기능들은 동일한 내용을 수행합니다. 하지만 불필요하게 A, B 이미지에 따라 구분되어 있었는데요.

    • S3 key 생성
    • empty 여부 확인
    • File → Stream 변환


    즉 하나의 기능 단위로 구분한 것이 아닌, 단순히 상황에 따라 함수만 분리한 것이 됐습니다.


    4) 조금 더 객체지향적으로

    클래스명은 설명을 위해 대략적으로 작성합니다.

    AwsService 클래스

    • 파라미터에 따라 다른 S3 key를 생성하는 함수

    Util 클래스

    • 파라미터에 따라 객체의 empty를 확인할 수 있는 함수

    StreamService 클래스

    • 파라미터에 따라서 Stream 형태로 변환해주는 함수


    이미지를 다운로드하는 최상위 객체가 존재합니다. 그리고 해당 객체는 AwsService, Util, StreamService 객체들과 대화합니다.

    ImageDownloadService가 말합니다.
    "내가 이미지 다운로드할 건데, 너네들이 가진 요 기능들 제공해줄 수 있어?"

    그리고 각각의 객체들은 역할과 책임을 가지고 대화하는데요.

    • AwsService "나는 AWS 관련해서 key 만들어주고, S3 object도 전달할 수 있는 도구들이 있어"
    • Util "난 딱히 특정 서비스는 아니고, 그냥 empty/null 확인을 하는데 집중할게"
    • StreamService "너네들이 파일 형태를 나한테 주면, 나는 stream을 만들어서 줄게!"


    즉 위와 같이 각각의 객체는 담당하는 역할이 생겼습니다.

    하지만 이런 역할을 담당하는 함수는 어떨까요?

    • getS3Key(path, user)
      • user 정보는 실패 시 로그를 작성하기 위해서만 필요한 파라미터

    getS3Key()의 경우에는 AWS가 필요한 대부분의 객체들에서 사용될 수 있습니다. 하지만 해당 객체들이 모두 user 정보를 알고 있을 수 없습니다. user 정보를 가지고 있지 않는 객체들은 해당 기능을 사용할 수 없게 됩니다. 즉 불필요한 외부 정보에 의존을 많이 하게 되는 구조입니다.

    그리고 getS3Key()는 오로지 S3 key를 응답하는데 역할이 있습니다. 실패했을 때 user 정보로 로그를 발생시키는 것은 getS3Key()의 역할이 아닙니다.

    그래서 위와 같이 불필요한 파라미터들도 제거하게 되었습니다. 그리고 해당 객체를 사용하는 부분에서 오류와 로그에 관한 역할을 맡게 되었습니다.


    5) 결론

    기존에는 하나의 클래스에서만 사용될 함수들로 구분되었습니다. 그리고 해당 함수들은 엄격한 의미의 역할과 책임이 구분되어 있지 않고, 상황에 맞게 그저 이름만 가진 함수였을 뿐입니다.

    또한 불필요한 파라미터들을 받으면서, 불필요한 외부 의존도가 올라가게 되었습니다.

    이를

    • 각각의 역할에 맞는 객체로 분리했고,
    • 하나의 기능에만 집중하고 책임을 가진 함수로

    구분했습니다.

    위의 작업으로 하나의 클래스에서만 동작하고, 그저 함수명만 지어서 구분해놓은 로직들을 다른 객체에서도 객체 간의 대화를 통해 사용할 수 있게 됐습니다. 그리고 동일한 작업을 수행하는 기능들을 상황에 따라 중복해서 만드는 것이 아닌, keyword를 바탕으로 객체가 대화할 수 있는 범위 내에서 활용할 수 있게 됐습니다.

     

    리팩토링은 언제나 재밌고, 배우는 게 많은 것 같습니다.

    반응형

    댓글

Designed by Tistory.