반응형
Spring MSA 프로젝트에서 단일 책임 원칙을 지키기 위한 리팩토링
우리팀은 MSA프로젝트에서 분산추적을 위해 Zipkin을 적용했다. 여기서 서버간 추적을 공유하기 위해서는 TraceId를 공유해야 한다는 것을 알게되었고 이에 SNS 메시지를 발행할때 snsClient.publish()메서드의 인자로 넣어주는 json 데이터에 TraceId도 포함해서 보내도록 로직을 수정했다.
이렇게 잘 사용하다가 우리는 코드 리뷰를 진행했는데 이때 평양냉면님이 이것은 단일 책임 원칙을 어기는것이 아닌가? 라는 질문을 해서 같이 이에대한 토론을 해본 결과 리팩토링을 진행하는것이 좋겠다고 결론이 나와 리팩토링을 진행했다. 오늘 포스트에서는 그 내용을 소개하고자 한다.
Zipkin 설정을 했던 내용이 궁금하다면? ⬇️ 아래의 글을 읽어보자!
1. 코드에서 단일 책임 원칙을 지키면 좋은 이유
1-1. 쉬운 유지보수와 이해하기 쉬운 코드
- 단일 책임 원칙을 따르면, 각 클래스나 메서드가 하나의 기능만을 담당해. 이는 유지보수를 쉽게 만들고, 코드를 처음 보는 사람도 무엇을 하는지 쉽게 이해할 수 있어. 예를 들어, 로그인 기능과 사용자 정보 관리를 분리하면, 로그인 방식을 바꿔도 사용자 정보 관리에 영향을 주지 않지.
1-2. 디버깅, 재사용, 테스트 용이성
- 버그가 발생했을 때 어디서 문제가 생겼는지 찾기 쉬워지고, 한 기능만 수행하는 코드는 다른 프로젝트에서도 재사용하기 좋아. 또한 테스트도 간단해져서 해당 기능만 확인하면 돼.
1-3. 확장성 향상
- 시스템이 커지거나 새 기능이 추가되더라도, 각 부분이 독립적으로 작동하기 때문에 하나를 변경해도 다른 부분에 큰 영향을 주지 않아. 이는 프로젝트의 확장성을 크게 향상시켜.
단일 책임 원칙(SRP)을 따르면, 코드가 더 깔끔하고 관리하기 쉬워진다. 복잡한 시스템에서는 특히 중요하지만, 상황에 따라서 유연하게 적용하는 것이 좋다고 한다. 왜냐하면 단일 책임 원칙(SRP)은 각 부분이 하나의 기능만을 담당하게 함으로써 코드의 복잡성을 줄이고, 이해 및 유지보수를 용이하게 만들어주는 것이다. 하지만 모든 상황에서 SRP를 엄격하게 적용하면 과도한 클래스 분리로 인해 오히려 관리가 복잡해질 수가 있다. 따라서, SRP는 프로젝트의 규모와 복잡성에 따라 유연하게 적용하는 것이 바람직하다.
2. 리팩토링이 필요한 코드 분석하기
2-1. 스프링 이벤트 리스너 메서드 분석하기
- 스프링 이벤트가 발행되면 아래의 리스너(구독자) 메서드가 동작해서 내부의 SNS발행 메서드인 publishNicknameToTopic()을 호출한다. 이때 메서드 내부에서는 memberId를 담은 messageJson을 생성해서 publishNicknameToTopic에 파라미터로 담아서 보내준다.
- 아래와 같은 CustomJsonBuilder 클래스를 사용해서 json 메시지를 만들어 줬다.
2-2. SNS메시지를 발행하는 publishNicknameToTopic 메서드 분석
- 위의 로직에 따라 이 메서드가 호출되는데 여기서는 크게 두 가지 일을 하고 있다. 첫째, TraceID를 추출하고, 둘째, SNS 메시지를 발행한다. 근데 여기서 처리하지 않아도 될것같은 로직이 존재하는데 바로 TraceID를 추출하고 Map으로 만들어서 다시 그것을 converMapToJson()메서드를 사용해서 json으로 변환하는 로직이다.
2-3. 잠깐! 지금 SNS발행 메서드는 단일 책임 원칙(SRP)를 어기고 있는것이 아닌가?
여기서 TraceID 추출은 로깅이나 모니터링을 위한 것이고, SNS 메시지 발행은 실제 비즈니스 로직의 일부인 상황이다. 두 기능은 서로 다른 이유로 변경될 수 있다. 예를 들어, 로깅 방식이 변경되면 TraceID 추출 로직이 바뀔 수 있고, SNS 메시징 요구사항이 변경되면 메시지 발행 로직이 바뀔 수 있는 것이다.
단일 책임 원칙(SRP)을 엄격히 적용한다면, 이 두 기능을 분리하는 것이 좋다. 하지만 실제 개발에서는 SRP를 절대적인 기준으로 삼기보다는 유연하게 적용하는 경우가 많다. 지금 나의 경우에는 TraceID 추출은 비즈니스 로직에 크게 영향을 미치지 않고, 주로 로깅이나 디버깅에 사용되니까 SRP를 크게 위반하는 것으로 보기 어렵다고 할 수 있을것이다.
하지만, 이 로직이 더 복잡해지거나 다른 기능들이 추가된다면, SRP를 다시 고려해야 할 필요가 있다. 예를 들어, TraceID 추출 로직이 더 복잡해지거나 다른 곳에서도 사용되기 시작한다면, 이를 별도의 메서드나 클래스로 분리하는 것이 좋을것이다.
2-4. 그래서 우리팀은 리팩토링 회의를 진행했다.
이 코드를 더 좋게 만들수는 없을까? 이런 생각에 계속해서 토의한 결과 우리는 다음과 같은 결론을 얻었다.
메서드 내부에서 TraceId를 추출하고 이것을 사용해서 새로운 json을 만들어주는 로직을 제거하고 이 메서드를 호출하는 스프링 이벤트 리스너에서 json을 만들어서 보내는데 이때 MemberId, TraceID를 같이 넣어서 json을 만들고 이값을 그대로 매게변수로 보내주자! 그럼 이 메서드는 SNS를 발행하는 작업만 하면 된다. 이렇게 되면 코드도 줄어들고 SNS발행 메서드는 단일 책임 원칙도 준수하게 된다.
3. 코드 리팩토링 진행하기
3-1. 스프링 이벤트 리스너 메서드 수정하기
- 이제는 메서드 내부에서 TraceId를 미리 추출해서 이 정보를 memberId와 같이 담아주고 customJsonBuilder를 사용해서 이 값들을 담은 messageJson을 생성해서 publishNicknameToTopic에 매게변수로 넣어줬다.
3-2. SNS메시지를 발행하는 publishNicknameToTopic 메서드 수정하기
- 이제 publishNicknameToTopic 메서드에서는 TraceId를 추출하고 json을 다시 만드는 작업을 할 필요가 없어져서 관련 코드를 다 제거했다. 이렇게 수정함으로써 publishNicknameToTopic 메서드는 SNS에 메지시를 발행하는 한가지의 일만 하게 되었다.
4. 리팩토링 후의 결과와 그 이점
4-1. 단일 책임 원칙(SRP)
- 리팩토링 이전의 코드에서 SnsService는 로깅을 위한 TraceID 추출과 SNS 메시지를 발행하는 두 가지 책임을 가지고 있었다. 하지만 두 번째 방식에서는 SnsService가 오로지 SNS 메시지 발행만을 담당하게 된다. 이렇게 하면 SRP를 더 잘 준수하게 되고, 각 클래스와 메서드가 명확한 역할과 책임을 가지게 되어 코드 유지보수가 용이해 진다.
4-2. 관심사의 분리(Separation of Concerns)
- 리팩토링 후에 SpringEventSnsPublishListener는 이벤트 리스닝과 관련된 로직(예: TraceID 추출)을 처리하고, SnsService는 단순히 메시지를 SNS로 발행하는 역할만 수행한다. 이렇게 관심사를 분리함으로써, 코드의 가독성과 관리 용이성이 증가한다.
4-3. 유연성과 확장성
- 만약 향후 로깅이나 추적 메커니즘이 변경되거나 확장되어야 할 경우, 리팩토링한 코드에서는 SpringEventSnsPublishListener만 수정하면 된다. SnsService는 수정할 필요가 없다. 반면 이전 코드에서는 로깅 방식이 변경될 때마다 SnsService도 수정해야 할 수 있다.
4-4. 로깅과 비즈니스 로직의 분리
- 리팩토링한 코드에서는 로깅/추적과 실제 비즈니스 로직(여기서는 SNS 메시지 발행)이 분리되어 있어서, 로깅/추적 로직에 의해 비즈니스 로직이 영향받지 않게 된다. 이는 특히 복잡한 시스템에서 중요하다.
4-5. 테스트 용이성
- SnsService가 단일 기능만을 수행하게 되면, 테스트가 더 쉬워진다. 메시지 발행 로직만을 테스트하면 된다.
5. 마무리
5-1. 결론
결론적으로, 우리팀은 이 메서드 리팩토링을 진행함으로써 단일 책임 원칙(SRP)을 더 잘 준수하고, 코드의 유지보수성, 가독성, 유연성을 향상시키는 장점을 가지는 코드를 작성했다.
5-2. 느낀점
이번에 단일 책임 원칙(SRP)을 지키기 위해 코드 리팩토링을 진행하면서 왜 이렇게까지 코드를 작성해야할까? 라는 생각이 들어 그 이유를 알아보고자 생각도 많이 하고 내용도 계속 찾아봤는데 그 이유를 알아볼수록 SRP를 지키는 것은 굉장히 중요하다는 생각이 들었다.
개발을 할때는 중요성을 중요성을 느끼지 못할수도 있지만 추가개발이나 운영 단계에 접어들었을때 이런 설계가 잘 이루어져 있다면 굉장히 편하게 유지보수가 가능하게 된다는 것을 알게되었다. 즉, 처음부터 잘 설계된 코드는 나중에 유지보수를 할 때 매우 큰 도움이 된다는 것이다.
나는 전 회사에서 서비스 운영을 담당했었는데 이 서비스는 10년도 넘은 규모가 큰 모놀리식 프로젝트였고 내부의 코드들은 급하게 작성한 부분들도 많고 여기저기 코드가 이어붙여져서 거의 SRP가 지켜지지 않은채로 코드가 작성되어 있었다. 그래서 내가 운영할때 간단한 수정사항을 부탁받아서 작업을 진행하려해도 하나를 수정하면 수도없이 많은 사이드 이펙트가 발생하고 확인이 필요해서 엄청 힘들었던 기억이 있다. 이런 경험을 떠올리니 더 SRP의 중요성이 크게 다가왔다.
우리팀은 이렇게 코드리뷰를 진행하고 수정도 해보면서 많이 성장해 나가고 있다고 생각한다. 최근들어 코드 리뷰를 진행하면서 많은 것을 배워가다보니 코드리뷰의 중요성을 많이 느끼고 있고 내가 생각하지 못한 부분은 팀원이 생각해내는 그런 선순환이 잘 이루어지고 있다고 생각한다. 항상 팀원인 평양냉면님께 감사하는 마음을 가지고있고 앞으로도 열심히 프로젝트를 진행할 것이다!
진행중인 MSA프로젝트 설계가 궁금하다면? ⬇️ 아래의 글을 읽어보자!
이 포스트는 Team chillwave에서 사이드 프로젝트 중 적용했던 부분을 다시 공부하며 기록한 것입니다.
시간이 괜찮다면 팀원 '평양냉면7'님의 블로그도 한번 봐주세요 :)
반응형
'Spring MSA' 카테고리의 다른 글
MSA 서버 간 통신: SNS의 MessageAttributes로 완벽한 Zeropayload 전략 구현하기 (1) | 2023.12.01 |
---|---|
MSA 환경에서 SNS 메시지 재발행을 위한 스프링 배치 및 스케쥴러 구현 (1) | 2023.11.28 |
Spring MSA: Sampling으로 원하는 http요청만 Zipkin으로 추적하기 (0) | 2023.11.23 |
Zipkin 로그 최적화: AWS ALB 헬스 체크 설정과 로그 추적 간소화 (2) | 2023.11.22 |
SpringBoot MSA 로깅: Zipkin을 사용한 분산 추적에서 예외상황을 다루는 방법 (1) | 2023.11.22 |