본문 바로가기
👨‍💻 프로그래밍/책잇아웃(포트폴리오)

😵 1달동안 MSA 다시 Mono로 전환한 후기

by 개발자 진개미 2023. 11. 13.
반응형

취준생 시절에 열심히 만들었던 책잇아웃이라는 독서 관련 사이트  있습니다. 처음에는 취업을 위해서 시작했지만 갈수록 제품에 애정이 생겨 취업 후에도 계속 운영하고 있었는데요. (운영이라고 해도 AWS에 서버 띄어 놓는 게 다이지만...)

문제는 프로젝트가 상당히 크고 복잡해서, 유지보수는 커녕 운영도 힘들다는 것이었습니다. 일단 서버 비용이 1달에 거의 20만원 가까이 나왔습니다... 9개의 서버를 동시에 띄워야 하는 MSA 구조 때문이었는데요, 서버 비용뿐만 아니라 여러 이유로 MSA 구조였던 프로젝트를 다시 Monolithic 구조로 전환하기로 하였습니다.

1달 동안 회사를 다니면서 주말에 틈틈이 프로젝트를 진행했는데, 없는 시간 쪼개서 하는 거니 기왕이면 뭔가를 더 하고 싶어 모두 Kotlin으로 재작성하고 일부 Refactoring도 진행했습니다.


기존의 구조 간단 설명

일단 처음에는 Monolithic으로 시작했습니다. 개인적으로 혼자서 나눈 버전이 있는데요. 잠깐 소개해 드리자면...

  • V1 (MVP) -> ID/PW 기반 로그인, 책 기록, 목표 설정
  • V2 -> OAuth 로그인, 검색
  • V3 -> 커뮤니티 (게시글, 댓글, 좋아요, 모임, 사이트 꿀팁 등)
  • V4 -> 도서관 관련 기능 (회원증 추가, 근처 도서관 검색, 도서관 별 책 검색 등)
  • V5 -> 알림 관련 기능

여기서 V3 쯤에 프로젝트가 너무 복잡하다고 생각해 MSA를 도입했습니다.

AWS에 배포돼 있던 버전 기준으로 소개하자면, 우선 Infra 관련 MS가 3개 있습니다. Spring Cloud Config 서버는 각종 Secret을 담고 있고, Eureka라고 불리는 Service Discovery를 써서 MS 간에 별명으로 소통하게 합니다. 그리고 공통의 관심사를 처리하고 트래픽을 분산하는 Spring Cloud Gateway가 있었습니다. (보안도 여기서 처리)

그리고 서비스 관련 MS가 6개가 있어 합쳐서 9개의 MS로 구성돼 있습니다. (굉장히 과하죠...?)


MSA를 버릴까

MSA의 단점만 경험하고 장점은 경험하지 못하고 있음

 

많은 개인 프로젝트가 그러겠지만, 책잇아웃은 트래픽이 0이라고 해도 과언이 아닙니다. (요즘은 바빠서 거의 유일한 사용자인 저도 자주 못 들어가니 진짜로 0일 지도...?) 그런데, MSA 때문에 개발 복잡도는 너무 높습니다.

Transaction을 처리하거나 오류가 발생했을 때의 경우의 수도 더 많고, Logging도 복잡하고, 핵심 비즈니스 로직이 아니라 Infra에 쏟는 시간이 너무 많습니다. 뿐만 아니라 Local에 개발할 때도 우선 Config 서버를 띄우고, Eureka를 띄운 다음 Gateway를 띄워야 개발 중인 MS에 요청을 보낼 수 있습니다. (Gateway에서 로그인 처리를 해서)

물론 회사에서 하듯 여러 Infra 적인 작업이나 자동화를 하면 마치 Monolithic에서 개발하는 듯한 경험을 낼 수 있습니다. 실제로 저도 회사에서 개발할 때는 배포할 때 빼고는 MSA 구조라는 걸 의식할 필요가 없을 정도로 편하게 잘 되어 있습니다. 하지만 사이드 프로젝트에서 이건 너무 과한 거 아닐까요?

애초에 제 서비스가 MSA를 개발할 정도로 큰 걸까요? 최근에 Amazon에서도 MSA로 돼 있던 서비스를 Mono로 합쳤더니 오히려 생산성은 증가하고 비용은 90% 이상 아꼈다는 기사도 있습니다.

가장 결정적인 계기가 됐던 건 회사에서 개발하면서 트래픽이 상당히 많이 나오고 복잡한 서비스인데 Mono로 잘 돌아가는 걸 보면서였습니다. 책잇아웃이 그 정도 트래픽을 훨씬 상회해서 MSA로 전환하지 않고는 안 되게 되면, 행복한 미소를 지으며 다시 MSA로 전환할 것 같습니다. 😁


또 버리시나요... 애초에 처음부터 왜 MSA를 썼는지?

좀 솔직하게 말하자면 취업을 위한 MSA 지식 습득60%, 정말 필요하다고 믿어서40%였습니다. 책 관련으로 시작했지만 책을 기록하는 것뿐만 아니라 여러 곳에서 검색하고, 도서관 관련 편의기능도 많고, 커뮤니티도 있고, 통계처리도 있어서 프로젝트의 복잡도가 상당히 높았습니다. 그래서 MSA로 분리하지 않고는 복잡도를 감당하지 못한다고 믿었습니다.

하지만 돌이켜 생각해 보니 관심사가 분리돼 있지 않고, 응집도가 낮고, 이상한 설계 때문에 복잡도가 올라간 거지 MSA가 만능은 아니었습니다. 정말로 규모가 큰 서비스라면 MSA 도입 없이는 관심사의 분리가 불가능하겠지만, 지금은 네카라쿠배당토직야 정도가 아니면 그 정도의 복잡도는 절대 아니라고 지금은 생각합니다.


전환하면서 겪은 다양한 어려움 잠담

우선 최악이었던 프로젝트별로 Dependency가 달랐던 것이었습니다. Java, Kotlin이 산재되어 있었고, 어떤 곳에서는 QueryDSL을 썼는데 어떤 곳에서는 안 쓰는 경우도 있어서 재작성이 생각한 것보다 많이 필요했습니다.

다음으로 굉장히 짜증 났던 건 프로젝트별로 비슷한 역할의 DTO, 심지어는 같은 DTO인데 미묘하게 데이터 타입이나 이름, 형식이 다른 경우가 너무 많았다는 것이었습니다. (Paging 처리할 때의 Response가 MS 별로 거의 다 다르고, MS 사이에 요청할 때의 DTO도 달랐습니다.) 처음에는 이걸 유지해 Front는 아무런 변경 없이 서버만 재배포하는 아름다운 그림을 원했지만 중간에 과감하게 포기하고 Front를 바꾸는 한이 있어도 DTO를 통일했습니다. (이때쯤부터 정신적으로 좀 힘들었습니다 🥲)

다음으로 MSA의 인프라 처리 코드를 분해했습니다. Circuit Breaker는 모두 에러로 바꿔서 throw 하거나 기본값을 주도록 하고, Open Fiegn를 사용해서 간결하긴 했으나 MS별로 호출이 있는 부분은 내부 코드 (Service, Repository 등)를 사용하도록 바꿨습니다. Gateway에 있던 JWT Token을 Parsing 해서 X Header로 넘기는 부분은 Spring Security의 Filter에서 SpringSecurityContextHolder에 저장하도록 refactoring 했습니다.

추가로 Java로 작성한 것을 Kotlin으로 변환하는 경우가 많았는데, IntelliJ의 기본 변환이 생각보다 별로여서 중간부터는 그냥 직접 작성했습니다. 작성하면서 Kotlin의 간결함에 다시 한번 놀랐습니다.

(코드가 너무 없으면 심심하실까 봐 JWT Token Parsing 하는 부분만...)

@Component
class JwtTokenFilter(
    private val jwtProperties: JwtProperties,
    private val jwtUtils: JwtUtils,
) : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val header = request.getHeader(jwtProperties.authorizationHeader()) ?: null
        
        val isHeaderValid = header != null && header.startsWith(jwtProperties.tokenPrefix)
        if (!isHeaderValid) {
            filterChain.doFilter(request, response)
            return
        }

        val jwtToken = header
            ?.replace(jwtProperties.tokenPrefix, "")
            ?.trim()
            ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
        val appUser = jwtUtils.parseAccessToken(jwtToken)

        val authentication = AppUserAuthentication(
            principal = appUser.email ?: "",
            credentials = null,
            authorities = appUser.authorities,
            id = appUser.id,
        )
        SecurityContextHolder.getContext().authentication = authentication

        filterChain.doFilter(request, response)
    }
}

 


전환 후 느낀 점

MSA에서 Mono로 전환 후 더욱더 크게 느낀 건 MSA의 본질은 결국 관심사의 분리라는 것입니다. 분명 여러 도메인의 코드가 1곳에 있는데 더 좋은 코드로 바꾸고 더 좋은 설계를 하니 그전보다 더 간결하고 깔끔해졌습니다. 특히 개인 프로젝트에서는 MSA를 도입하기 전에 기본으로 돌아가 SOLID 원칙을 지킨 코드인지, 응집도가 높고 결합도는 낮은 설계인지를 더 고민할 것 같습니다.

(가장 중요한?) AWS 비용은 17만 원에서 3만 원으로 줄었습니다!

중간에 Mono 전환을 해서 높아 보이지만 비율을 계산하면 1달에 3~5만원이랍니다... 😅


반응형

댓글