ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot Version 올리기 - feat. 순환 참조(Circular References)
    Spring/Spring Boot 2023. 8. 28. 08:47
    반응형
    이젠 놓아주자...

     

    Spring boot 3 가 드디어 출시 하였다 .. 라고 하기엔 좀됬다 ... 

    Spring framework 5.0 을 이제 슬슬 놓아 줘야 하겠지만.. 아직 현장에는 Spring boot 1.x 버전도 돌아다니는곳이 많다. 

    Spring boot 를 현재 2.1 버전으로 구축 되있는 영역을 버전을 높이기위한 준비를 진행 하기로 했다. 목표는 Spring boot 3 이 목표다 

    이유는 아래와 같다. 

    1) Spring boot 2.7 버전 지원이 몇년 남지 않았다. 
    2) Java 8 은 이제... 그만... 
    3) 라이브러리들의 호환성... 챙겨 보자 
    4) 프레임워크의 버전이 높아 짐에 따라 가져갈수 있는 안정성 

     

    일단 , 버전업을 하기전에 여러가지 검토가 필요하다. 버전을 어디까지 올릴수 있을것인가 현재 구조가 적합한가 ? 아니면 차선책이 있는가 등등등.. 

    Spring boot 3 으로 올리기위한 여러가지 준비사항을 보면 기본적으로 두가지가 충족되어야한다. 

     1) Spring boot 2.7 이상 버전으로 먼저 준비가 되어야한다. 
    2) Java 17 이상으로 버전을 변경 한다. 

     

    이 두가지만해도 굉장히 큰 난관에 봉착한다. 이유는 여러가지지만 일단 Spring boot 버전 부터 생각해 본다. 현재는 2.1 버전을 사용중이다 초창기 2의 버전라인이고 생각보다 2.7까지 오면서 많은것들이 변경 되었다. 물론 주기적으로 버전을 업그레이드 하면서 보면 큰 부담이 없겠지만 2.1 버전에서 한번에 2.7 까지 가기 위해선 버전 올리는것 하나만으로도 큰 문제들을 야기한다. 

     

     <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <!--                <version>2.3.12.RELEASE</version>-->
            <!--                <version>2.5.14</version>-->
            <version>2.7.7</version>
            <!--                <version>2.1.6.RELEASE</version>-->
            <!--                <version>2.4.13</version>-->
            <relativePath />
            <!-- lookup parent from repository -->
        </parent>

     

    이 부분은 노력의 흔적이다. 시작은 2.1.6.. 그리고 3,4,5... 그리고 2.7 까지 버전을 하나하나 올리면서 체크했다. 결론부터 말하면 2.7.7까지 버전업을 성공하였다. 솔직히 버전업이 성공하는것은  프로젝트의 코드 구성의 영향이 70% 이상이다. 그이유는 차차 언급하겠지만 기존 시스템을 Spring boot 3까지 올리겠다는 의지가 있다면 미리 말하지만 고행의 길을 감수할 각오를 해야한다. 

     

    목표 부터 ! 

     

    일단 프레임워크 버전을 먼저 올리는 단계이기 때문에 jvm 은 그대로 1.8을 유지 했다. 만일 프레임워크과 jvm 버전 작업을 한번에 하겠다는 분은 말리고싶다. 프레임워크 작업이 먼저다 ! jvm 의 하위 호환성은 프레임워크보단 좋기 때문에 .. ( 꼭 그렇지도 않다 .. ㅠㅠ ) 손이 많이 가는 프레임워크 부터 작업한다. 

     

    라이브러리 Check! 

     

     버전이 올라가면 중요한 부분들이 많이 변경 되는데 그중 가장 신경 써야하는것은 기존 사용하던 spring boot 라이브러리의 패키지 경로 변경이다.  기존 Sun 사 시절 잔재들이 새로 패키지명이 바뀌어 편입되는 케이스도 있고 특히 엑셀을 사용하는 중이라면 엑셀 사용할때 사용하는 라이브러리에 대한 대응을 꼼꼼히 봐야한다. 

     

     그렇기 때문에 현재 사용버전에서 내가 목표로하는 버전까지 각각의 중요 변경점을 먼저 체크해봐야한다. 이게 가장 중요한 작업일 수 밖에 없는 부분은 생각보다 많은 코드 에서 에러가 나게 되고 코드에서 자주사용하는 빈도가 높은 코드에서 문제가 생긴다면 작업량이 그만큼 많아 질수 밖에 없다. 

     

    약간의 요령이 있다면 버전을 일단 최대로 무지성으로 올리고 빌드해보면서 체크해봐도 된다. Java 는 다른 스크립트 언어와 다르게 컴파일러 언어로써의 장점을 최대한 활용하면 나름 시간을 단축할수 있다. 하지만 이렇게 해보니 생각보다 에러 문구들이 직관적이지 못했고 실제로 원인이 되는 부분의 대한 로그를 확인하기 힘들었고 원인으로 인해 2차로 발생하는 에러에 대한 문구들이 나오다보니 분석하는데 굉장히 힘들긴 했다. 

     

     버전을 올리다보면 그다음 놓치기 쉬운부분은 로그 부분이다. 버전없이 되면서 로그쪽도 Spring 에서 기본적으로 제공하는 설정과 기존에 외부 라이브러리를 선언하여 사용중인 부분이 있다면 이 영역이 겹쳐서 충돌이 나지만 실제로는 아무런 에러가 나지 않아 로그가 정상적으로 안찍힐수 있기 때문에 Maven/Gradle 에서 어떤 라이브러리에 우선 순위를 줄지에 대해서 잘 드라이빙을 해줘야한다.  아래는 외부 라이브러리를 사용하기위해 기본 제공되는 Logback 설정을 제외한 케이스이다. 

     

     <!-- Springboot -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-logging</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>

     

    어느정도 체크를 했다면 열심히 라이브러리 교체를 하면서 체크해줘야한다. 이게 maven/gradle 에서 각 라이브러리중 버전에 대한 Reimport  작업 무한 반복이다. IDE 툴을 최대한 활용해야한다. 모든 라이브러리에 명시된 버전은 실제로 spring boot 목표 버전과 호환이 안될수 있어 항상 maven repository 사이트를 옆에 오픈해두고 체크해봐야한다. 가장 편한 방법은 spring boot 버전에 depengency 를 걸어서 버전을 맞게 다운로드 받도록 하는것인다. 이럴때 위에 로그 케이스 처럼 spring boot 에서 편하라고 기본 탑재 해준것들이 굉장한 부비트랩으로 발동될수 있으니 유의해야한다. 

     

    아... 순환참조 

     

     라이브러리가 어느정도 맞춰지면 Spring boot 가 구동 될것이다. 하지만 아마도 라이브러리가 교체되고 버전없되면서 일단 특정 클래스들에 import 패키지 경로들에서 에러가 날것이고 이것부터 맞춰주는것이 먼저다! 이걸 맞추다보면 자연스럽게 내가 버전업한 라이브러리들이 기본적으로 어떤것들이 사라지고 어떤것들이 대체되고 이런것들을 찾게 될것이고 다 맞춰주게 되면 거의 절반의 성공은 했다고 본다. 

     일련의 작업이 끝나고 실제 코드를 동작 했는데 .... 순환참조가 있었다 아주 여러건 ....

     

    참조 : https://ch4njun.tistory.com/269

     

    위에 이미지는 타 블로그에서 캡쳐해서 온것인데 ( 지금은 해결해서 일부로 내기가 애매한지라... ) 이런 생소한 에러가 나면서 어플리케이션 구동에 실패하게된다. 

     

    닭이 먼저? 달걀이 먼저 ? 니가 먼저 ? 내가 먼저 ?

     

     간단하게 설명하면 A란 녀석이 실행되려면 B 란녀석을 데리고 와야 되는데 B 란 녀석이 일을 하려면 A란  녀석을 자기한테 데려와야지만 일이 가능하게 된다. 이럴때 A와 B란 녀석이 같이 수행되면 A가.. 실행되려면 B부터 ..데꼬와야하고... B부터 ..하려면 A..가.. A가 실행해야하니 그럼 B가... 

     

     이런 이상한 무한 참조에 빠지게 된다. 이런 현상은 Bean 객체를 생성하여 사용할때 생기게 되고 이럴경우 가장 큰 문제는 메모리 누수의 원인이 된다. java 에서 메모리 누수라니... 똑똑한 jvm 이라지만 만든사람이 저리 만들면 어쩔수 없는 케이스이다. 

     

    spring boot 2.1 에서는 이 부분이 기본적으로 체크하지 못한다 일단 전부 bean 객체를 생성하기 때문에 딱히 알수 없다. 그러면 저 에러가 나는 시점은 확인했을때 2.3 버전부터 생겼던것으로 기억한다. 결론적으로는 spring 에서는 순환참조를 원칙적으로 허용하지 않겠다는 방침을 가지게 된것이고 이것을 최초 구동시점부터 체크하게 변경 되었다는 것을 확인할수 있다. 

     

     메모리누수는 운영 단계에서 굉장히 디텍팅하기 힘든부분이고 찾았더라도 원인을 확인하기 굉장히 어려운 부분이다. 이런부분을 프레임워크에서 사전방지를 해준다는것은 굉장히 좋은 부분이라고 생각한다. 

     

    순환참조는 클래스간 상호 참조를 해서 생길수도 있지만 가끔 코딩 하시는 분들중 @Service 클래스 생성시 클래스 자기 자신의 변수를 가지게해서 선언하는 경우도 있다. 이런경우 셀프 순환참조가 되어 이것도 동일하게 Spring boot 에서 허용치 않는다. 이전에는 보통 트랜잭션의 유지를 위해 자기자신 클래스 변수를 선언하는 케이스들이 많았는데 안된다! 

     

     그렇기 때문에 아래와 같은 원칙으로 코딩하고 기존 구조를 바꿔야한다. 

     

    1) 자기 자신의 메서드를 사용할땐 this 와 private 메서드를 사용한다. 트랜잭션을 걸어야할경우 해당클래스를 위한 sub 클래스를 별도로 선언하여 사용한다.
    2) 여러곳에서 참조하는 클래스가 있다면 이 클래스는 도메인 기반의 공통의 클래스를 선언하여 사용 한다.
    3) 기본적으로 클래스의 의존성을 설계할때는 Linked 하게 만들지 않고 Tree 구조의 참조관계를 가지게 사용한다.

     

    실제로 버전을 변경하면서 여러가지 포인트에서 순환 되는 케이스들이 확인됬고 클래스자체를 재설계해서 배치하기 시작했다. 이부분이 가장 고통스러운시간 이다. 결국엔 순환참조가 걸려있는 클래스들을 메서드들을 다시 재배치 하기 시작하였고 그에 따라 일부 로직적 부분도 다시 수정 하게 되었다.  굉장히 복잡한 작업이 될수 있어서 아래와같은 순서대로 진행했다. 

     

    1) 순환 참조되는 메서드들을 하나의 공통 클래스에 모아놓은다 ( 이때 도메인에 따라 분리하여 , 수집 ) 
    2) 메서드를 모두 모았다면 순서대로 해당 선언된 실제 메서드를 사용하는 부분을 하나씩 차근차근 참조 경로를 수정한다. 
    3) 수정후 에러나는 케이스들을 체크하고 로직의 변경이 필요하다면 최소한의 로직 변경을 진행 한다. 
    4) 자기 자신을 셀프 순환 참조하는 클래스는 동일한 이름의 자식 클래스를 만들어서 별도의 트랜잭션처리가 가능하도록 변경 한다. 

     

    간단하게 표현했지만 이작업이 전체 작업중 가장 오래 걸린 부분인것 같다. 현재는 이작업들을 적용하여 실제 서비스에 크게 문제없이 2.7버전으로 구동중이다. jvm버전 업을 진행 하려고 했으나 아이러니하게 현재 시스템구성이 임베디드톰캣이 아닌 외장 톰캣을 사용하고 있어서 톰캣 버전부터 다 변경해야하다보니 이부분은 아무래도 테스트 이런것들이 섬세하게 진행할 부분이 많다. 그래서 일단 잠시 보류! 

     

    달리는 자동차에서 바퀴를 갈아야하는 개발자의 숙명은... 역시.. 어쩔수 없는것 같다. 이제 앞바퀴를 교체했고 뒷바퀴가 남았는데.. 녹녹치 않은것 같다. 

     

    이번에 작업을 해보면서 느낀건 클래스 설계의 중요성.. 이건 학생때 분명 배우지만 실제로는 우린 코딩을 할때 클래스 설계를 그렇게 디테일하게 공들여 하지 않는데 왜냐하면 경력이 늘면서 이 설계란게 요구사항에 따라 결국 계속 바뀔수 밖에 없단걸 알고 그게 관성처럼 적응이 되기 때문인거 같다. 하지만 이런 아주 기본적인 부분들은 잘 생각해서 메서드를 만들때도 그냥 아무생각없이 만드는것이 아니라 어떤 클래스에 어떤 방법으로 구성할지 생각하는 정도만 있어도 이렇게 고생하는 일은 없을것으로 보인다. 

     

    그리고 두번째는 테스트 코드의 유무 ... 테스트코드를 처음 일배우기 시작하고나서 몇년동안은 존재조차 몰랐던것 같다. 연차가 쌓이면서 중요성을 느끼고 있고 이제는 일단 기능을 만들어볼때 테스트코드로 대략의 메서드단위의 기능을 만들어보고 실제 적용을 해보는것 같다. 이렇게 되기까지.. 참 오랜시간이 걸렸고 아직도 시간이 없거나 너무 단순한 업무는 아직도 테스트코드를 짜지 않는 경우가 많다. 

     

    이번에 이 작업을 하면서 테스트코드가 너무 절실했다. 이유는 라이브러리 교체나 각각의 클래스가 재설계 되었을때 이게 정말 제대로 동작하는것에 대한 신뢰는 컴파일러가 해주는 빌드 레벨로는 한계가 명확하다. 우리는 실제로 빌드되고 구동된뒤에 정말 잘 응답을 서버가 내려주는지에 대한 것이 중요하기 때문이다. 이 부분을 100%는 아니지만 개발자로 하여금 안도감을 조금이라도 줄수 있는건 테스트코드란 녀석일 것으로 생각한다.  

     

    충분히 할수있다고 생각하고 꼼꼼하게만 준비하면 누구든지 할수 있다고 생각한다. 버전업을 보통 생각할때 대다수는 잘돌아가는것을 왜 건드냐고 핀잔을 주거나 부정적인 피드백을 주는 경우가 많다. 

     

    소프트웨어는 사람이 만들기에 불안정하고 사람들이 만들기에 그것만의 라이프 사이클이 존재한다. 언젠가는 라이프사이클의 끝이 보이는 시점의 소프트웨어를 만났을때 그 소프트웨어를 잘 승계 하기 위해서라도 ! 그리고 조금이라도 라이프사이크를 연장해서 그 끝을 좀더 길게 늘려 안정적인 서비스를 제공하기 위해서라도 버전업하는 작업 자체에 대해 조금이라도 덜 보수적으로 접근 해보는건 어떨까 싶다! 

    반응형
Designed by Tistory.