반응형

https://github.com/Seungkyu-Han/Toge-do-backend

 

GitHub - Seungkyu-Han/Toge-do-backend: Toge-do 앱의 백엔드 리포지토리입니다

Toge-do 앱의 백엔드 리포지토리입니다. Contribute to Seungkyu-Han/Toge-do-backend development by creating an account on GitHub.

github.com

얼마 전에 게임회사의 면접을 보게 되었다.

해당 면접에서 Heap 영역에서의 메모리 단편화를 막기 위해서는 어떤 방법이 좋을까요?라는 질문이 있었다.

 

그 당시에는 답변을 하지 못했지만, 지금와서 생각해보면 이 ObjectPool을 적용하는 것이 답일 것 같다.

 

잊지 않기 위해 현재 진행하고 있는 프로젝트에 ObjectPool을 적용해보려 한다.

 

직접 만들어야 하나... 생각하고 있었는데

다행히 스프링에서 해당 라이브러리가 존재한다.

 

일단 Object Pool은 객체가 필요할 때, 매번 생성하는 것이 아니라 관리하고 있는 pool에서 가져오는 것을 말한다.

이렇게 Pool에서 객체를 가져오게 되면, 우선 객체를 생성하는 데 필요한 비용이 감소한다.

객체를 생성할 때, 메모리를 할당하고 객체를 생성하는 과정은 시간이 굉장히 오래 걸리기 때문에 이 과정을 아낄 수 있다.

또한, 불연속적인 Heap 메모리에 계속 할당하지 않기 때문에 메모리의 단편화가 줄어들게 된다.

찾아보니 게임업계에서 굉장히 많이 사용하는 방법인 것 같다.

 

바로 적용을 해보도록 하자.

 

    //OBJECT POOL
    implementation("org.apache.commons:commons-pool2:2.12.1")

 

해당 라이브러리를 추가해준다.

 

java에서 Object Pool을 관리해주는 라이브러리이다.

 

그 다음에는 Pool에서 관리할 객체를 넣어주도록 하자.

나는 UserDocument를 Pool에서 관리하도록 할 것이다.

 

@Component
class UserPoolComponent: BasePooledObjectFactory<UserDocument>() {

    private var a = 0

    override fun create(): UserDocument {
        a += 1
        return UserDocument(
            name = a.toString(),
            oauth = Oauth()
        )
    }

    override fun wrap(userDocument: UserDocument): PooledObject<UserDocument> {
        return DefaultPooledObject(userDocument)
    }
}

 

우선 create 메서드를 오버라이딩해서 미리 저장해둘 객체들을 만들어준다.

이 create 메서드를 통해 객체를 생성해서 pool에 저장해두게 된다.

 

wrap 메서드를 통해 해당 객체를 관리하도록 하는데, 이것도 DefaultPooledObject를 통해 라이브러리를 사용해 객체를 관리할 수 있다.

wrap을 통해 해당 객체의 대여상태, 유휴상태등을 저장하고 관리하게 된다.

 

그리고는

val userPool = GenericObjectPool(userPoolComponent, GenericObjectPoolConfig<UserDocument>().apply{
        maxTotal = 10
        minIdle = 5
    })

해당 component로 pool을 만들어서 사용한다.

 

해당 pool에서 borrowObject를 통해 객체를 대여하고, returnObject를 통해 객체를 반납한다.

당연하게도 대여한 후에 반납을 꼭 해줘야, 다음 사용자가 사용이 가능하다.

 

그리고 중요한 부분은, 반납하는 순간은 해당 함수의 종료 시점이 아니라 해당 함수를 호출한 함수가 해당 객체의 사용이 끝날 때이다.

호출당한 함수에서 반납을 해버리면, 동시성 문제가 발생 할 수 있다.

 

일단 테스트를 진행해보도록 하자.

 

 

        @Test
        @DisplayName("Object Pool 테스트")
        fun createUserObjectPoolTest(){
            //given

            for (i in 1..10){
                val a = userService.userPool.borrowObject()
                println(a)
            }

            //when

            //then
        }

 

이렇게 10개를 반납하지 않고, 호출해보았다.

객체들의 이름은 알기 쉽도록 1부터 10까지 차례로 생성해보았다.

 

이렇게 반납을 하지 않으니, 계속 객체풀에서 다음 객체가 대여되어 다른 이름을 가지는 것을 볼 수 있다.

 

        @Test
        @DisplayName("Object Pool 테스트")
        fun createUserObjectPoolTest(){
            //given

            for (i in 1..10){
                val a = userService.userPool.borrowObject()
                println(a)
                userService.userPool.returnObject(a)
            }

            //when

            //then
        }

 

이렇게 반납을 하면서 출력을 해보니,

 

하나의 객체만을 사용해서 출력이 되는 것을 볼 수 있다.

 

Object Pool이 객체 생성에 대한 비용을 아낄 수는 있지만, 해당 객체의 사용 종료 시점을 추적해야 하는 등 생각보다 사용이 어렵다.

동시성 문제를 피하기 위해서는 정확하게 추적이 가능할 때만 사용하도록 하자.

반응형

전날에 작성해 놓은 테스트를 추가하기 위해, 아침부터 테스트 코드를 작성하던 중

@MockBean이 적용되지 않는 문제가 발생했다.

 

아예 사용할 수 없도록 되어버린 것 같고, 대체가 어떻게 되었을까 찾아보다가 아래와 같은 글을 발견했다.

 

https://stackoverflow.com/questions/79243535/what-is-the-replacement-for-the-deprecated-mockbeans-in-springboot-3-4-0

 

What is the replacement for the deprecated @MockBeans in SpringBoot 3.4.0?

We defined an annotation for our tests to avoid to declare Mockito Mocks for all the test classes. @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @MockBeans(...

stackoverflow.com

 

@MockitoBean

이것으로 대체가 되었다고 한다.

 

 

이렇게 하니 적용이 되는 것을 볼 수 있다.

 

다른 프로젝트에서는 되던 것이, 버전이 바뀌었다고 되지 않아서 깜짝 놀랐었다...

반응형

프로젝트를 시작할 때마다, 가장 먼저하는 CI/CD 설정이다.

기존에는 대부분 Jenkins를 통해 CI/CD 설정을 했었지만, 이번 프로젝트는 별도의 서버를 구성할 여유가 없어 Github에서 제공하는 Github action을 통해 설정을 해보려고 한다.

 

우선 Github의 리포지토리에서 actions 탭을 클릭한다.

그러면 이런 식으로 페이지가 나올텐데, 여기에 gradle을 검색한다.

 

그리고 이 action을 configure해준다.

 

이거 클릭하면 코드의 .github 폴더에 yml 파일이 생길텐데, 해당 파일을 수정해주면 된다.

 

크게 build와 deploy로 나뉜다.

build는 Gradle을 바탕으로 spring을 빌드하는 부분이고, deploy는 빌드된 jar파일을 서버로 전송하여 배포하는 부분이다.

 

name: CI/CD for develop branch

on:
  push:
    branches: [ "develop" ]
  pull_request:
    branches: [ "develop" ]
  workflow_dispatch:
    

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
    - uses: actions/checkout@v4
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582

    - name: Grant execute permission for gradlew
      run: chmod +x ./gradlew
      working-directory: ./WAS

    - name: Build with Gradle Wrapper
      run: ./gradlew build
      working-directory: ./WAS
      
    - name: Scp to EC2
      uses: appleboy/scp-action@master
      with:
        host: ${{ secrets.SERVER_IP }}
        username: ${{secrets.SSH_USER}}
        key: ${{secrets.SSH_PRIVATE_KEY}}
        source: |
          WAS/build/libs/{빌드된 파일 이름}.war
        target: was/
      
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.5.4
        with:
          ssh-private-key: ${{secrets.SSH_PRIVATE_KEY}}
          
      - name: Deploy Docker Container
        run: |
          ssh -o StrictHostKeyChecking=no ${{secrets.SSH_USER}}@${{secrets.SERVER_IP}} << 'EOF'
          cd was
          docker stop {컨테이너 이름} && docker rm {컨테이너 이름} && docker rmi {이미지 이름}:latest
          docker build -t {이미지 이름} .
          docker-compose up -d
          EOF

 

우선 작성한 파일은 다음과 같다.

 

step에서 하나씩 뜯어보도록 하자.

  • build
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582

이 부분은 자바와 gradle을 준비하는 부분이다.

큰 이상이 없다면 아마 고정적으로 사용할 것 같다.

 

    - name: Grant execute permission for gradlew
      run: chmod +x ./gradlew
      working-directory: ./WAS

    - name: Build with Gradle Wrapper
      run: ./gradlew build
      working-directory: ./WAS

이 부분은 빌드하는 부분이다.

gradlew에 실행 권한을 부여하고, ./gradlew build를 통해 스프링부트를 빌드한다.

만약 테스트와 관련된 명령어가 있다면, 이곳에 추가하면 된다.

 

아래를 보면 working-directory가 설정되어 있는 것을 볼 수 있는데, 만약 빌드하는 폴더가 루트 폴더가 아니라면 해당 명령어를 통해 폴더를 지정해주어야 한다.

 

    - name: Scp to EC2
      uses: appleboy/scp-action@master
      with:
        host: ${{ secrets.SERVER_IP }}
        username: ${{secrets.SSH_USER}}
        key: ${{secrets.SSH_PRIVATE_KEY}}
        source: |
          WAS/build/libs/{빌드된 파일}.war
        target: was/

이제 빌드된 jar(war) 파일을 서버로 전송하는 부분이다.

당연히 서버의 ip, 사용자 이름, pem 키가 필요하다.

이런 부분은 yml 파일에 작성할 수 없기 때문에, 해당 Repository의 설정에 환경변수로 추가해주도록 하자.

 

pem키를 환경변수로 추가할 때는 위, 아래 boundary까지 그냥 추가해주면 된다.

source는 빌드된 파일(결과물)이고, target은 해당 서버에서 어떤 폴더에 저장할 것인지를 적어주면 된다.

 

이렇게 작성하면 빌드 후, 파일이 서버에 저장되게 된다.

그럼 이제 해당 파일을 통해 배포를 설정해주어야 한다.

 

 

  • deploy

해당 jar 파일을 docker container를 통해 실행해보자.

 

    needs: build

needs에 build를 추가해서, 해당 작업이 build 이후에 이루어질 수 있도록 해준다.

 

 

      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.5.4
        with:
          ssh-private-key: ${{secrets.SSH_PRIVATE_KEY}}

서버에서 사용할 pem키를 등록해준다.

 

      - name: Deploy Docker Container
        run: |
          ssh -o StrictHostKeyChecking=no ${{secrets.SSH_USER}}@${{secrets.SERVER_IP}} << 'EOF'
          cd was
          docker stop {컨테이너 이름} && docker rm {컨테이너 이름} && docker rmi {이미지 이름}:latest
          docker build -t {이미지 이름} .
          docker-compose up -d
          EOF

 

그리고는 이제 원격 서버에 접속해 해당 파일을 바탕으로 도커를 빌드한다.

원격 서버에서 추가적으로 실행하고 싶은 부분이 있다면, 저 안에 추가해주면 된다.

 

이제 작성이 모두 완료되었으니, 실행해보도록 하자.

위의 on에 workflow_dispatch:를 지정해주면 직접 실행 할 수도 있다.

 

 

이렇게 초록색으로 체크표시가 된다면 성공한 것이다.

서버에서도 배포가 완료된 것을 확인 할 수 있었다.

'틔움랩' 카테고리의 다른 글

Spring에서 DeepL API를 사용해 문서를 번역하기  (0) 2025.02.27
MockBean deprecated와 대체  (0) 2025.01.28

+ Recent posts