1. GC라는 것은 무엇인가?

GC(Garbage Collection)는 Java에서 더 이상 사용되지 않는 객체를 정리해주는 메모리 관리 기능이다. 객체 그림 가능성을 고려하지 않고, 객체가 더 이상 참조되지 않는 경우 자동으로 해제한다.

대표적인 GC 알고리즘:

  • Serial / Parallel GC: Throughput 중심
  • CMS: 응답 시간 최소화
  • G1GC: Region 기능을 활용해 큰 heap에서 효율가 높은 GC

2. 실전 사례: 무한 루프 → Full GC 연속 발생 → WAS 달성

현재 Java 여보 앱에서 다음과 같은 증상이 발생했다:

  • 일정 시점 이후 CPU 100% 고정
  • GC 로그에 Full GC가 발생하고 다음과 같은 메시지 표시: Pause Full (System.gc())
  • 결과적으로 WAS가 응답 불가 상태에 빠짐

또한, G1GC 로그에서는 다음과 같은 심각한 메시지가 확인되었다:

  • Evacuation Failure: Young GC에서 객체를 이동시킬 공간이 부족
  • to-space exhausted: 힙 내에 여유 공간이 없어 객체 복사가 불가능
  • Full GC (Allocation Failure): GC가 메모리를 확보하지 못해 강제로 Full GC 발생

GC 후 사용량이 used=6283241Kused=6283241K로 변화 없이, 실질적으로 정리되지 못한 객체가 그대로 유지됨을 보여준다. 이는 힙이 포화 상태에 도달한 후에도 메모리를 회수할 수 없는 상황이었다.


3. Heap Dump 분석을 통한 문제 파악

Heap Dump 수집:

jmap -dump:format=b,file=heapdump.hprof <PID>

분석 도구: Eclipse MAT (Memory Analyzer Tool)

분석 결과:

  • 특정 VO 클래스가 List에 계속 객체를 add()하고 size를 키우는 구조 구조
  • 참조가 끊기지 않아 GC 대상이 되지 않고 메모리 문제가 발생

예시 코드:

for(int i = 0; i < list.size() ; i++){
	list.add(AAA);
    //기타 문서 처리
}

특정 시점에 진입하는 트랜잭션 일부가 이 루프에 빠져 종료되지 않음으로써, Full GC가 연속적으로 발생했고 최종적으로 WAS 다운까지 이어졌다.


4. 조치 및 확인

  1. 무한 루프에 종료 조건 추가
  2. 해당 트랜잭션을 단일 환경에서 재현 후 테스트
  3. GC 로그 확인을 위한 JVM 옵션 추가:
-XX:+PrintGCDetails -Xloggc:/var/log/gc.log

Heap 크기 일시 증설 (-Xmx10g)


5. 결론 및 교훈

  • Full GC는 결과일 뿐, 그 원인은 따로 있다.
  • Heap Dump는 실시간 분석에서 중요한 도구이다.

Redux Toolkit(RTK)은 Redux 공식팀에서 제공하는 상태관리 툴킷으로, 개발자들이 겪는 복잡한 설정과 보일러플레이트 문제를 해결해줍니다. 특히 현업에서 많이 쓰이는 기능을 잘 알고 있으면, 개발 생산성과 유지보수성이 모두 크게 향상됩니다.

이 글에서는 RTK에서 가장 자주 사용되는 기능 5가지를 실전 코드와 함께 자세히 소개합니다.


1. createSlice – 상태와 리듀서를 하나로 통합

createSlice는 RTK의 핵심 기능 중 하나로, 상태 초기값, 액션 생성, 리듀서를 하나의 선언형 코드로 관리할 수 있게 해줍니다. Redux의 대표적인 단점인 ‘액션 따로, 리듀서 따로’ 구조를 없애줍니다.

예시 코드

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1 },
    decrement: (state) => { state.value -= 1 },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

state.value += 1처럼 불변성 걱정 없이 코드를 작성할 수 있는 건 RTK 내부에 Immer가 기본 내장되어 있기 때문입니다.


2. createAsyncThunk – 비동기 요청과 상태를 동시에 관리

기존 Redux에서 비동기 요청을 다루기 위해선 Redux-thunk 또는 Redux-saga 등을 별도로 설정해야 했습니다.
하지만 RTK는 createAsyncThunk를 통해 비동기 API 호출과 상태 관리를 하나의 흐름으로 처리할 수 있습니다.

예시 코드

import { createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'

export const fetchUser = createAsyncThunk('user/fetchUser', async (userId) => {
  const response = await axios.get(`/api/users/${userId}`)
  return response.data
})

createAsyncThunk로 만들어진 액션은 자동으로 3단계 상태를 가집니다:

  • pending: 요청 중
  • fulfilled: 요청 성공
  • rejected: 요청 실패

이를 extraReducers에서 다음과 같이 처리합니다:

extraReducers: (builder) => {
  builder
    .addCase(fetchUser.pending, (state) => {
      state.status = 'loading'
    })
    .addCase(fetchUser.fulfilled, (state, action) => {
      state.status = 'succeeded'
      state.user = action.payload
    })
    .addCase(fetchUser.rejected, (state, action) => {
      state.status = 'failed'
      state.error = action.error.message
    })
}

 

3. createEntityAdapter – 리스트 상태를 효율적으로 관리

여러 개의 데이터를 리스트 형태로 관리할 때는 createEntityAdapter가 매우 유용합니다. 예를 들어, 사용자 목록, 게시판 글 리스트 등에서 다음과 같은 기능이 필요합니다:

  • 데이터 정렬
  • id 기반 조회
  • 빠른 추가/삭제/수정

기능 예시

import { createEntityAdapter } from '@reduxjs/toolkit'

const usersAdapter = createEntityAdapter({
  selectId: (user) => user.id,
  sortComparer: (a, b) => a.name.localeCompare(b.name)
})

const initialState = usersAdapter.getInitialState({
  loading: false
})

usersAdapter는 다음과 같은 메서드를 제공합니다:

  • addOne, addMany, removeOne, updateOne
  • selectAll, selectById 등 selector도 자동 생성

복잡한 리스트를 단순한 객체 조작처럼 다룰 수 있어, 실무에서 매우 자주 사용됩니다.


4. RTK Query (createApi) – 서버 상태를 자동으로 관리

RTK Query는 서버에서 데이터를 가져오고, 캐시하고, 리패치하고, 무효화하는 과정을 자동화해주는 기능입니다.
React Query와 유사하지만 Redux 상태와 자연스럽게 통합됩니다.

기능 요약

  • API 요청을 캐시하고 자동으로 재요청
  • useQuery, useMutation 훅 제공
  • 요청 상태를 자동 관리 (isLoading, isError, data 등)
  • 자동으로 re-fetch, cache invalidation 가능

예시 코드

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const userApi = createApi({
  reducerPath: 'userApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getUser: builder.query({
      query: (id) => `/users/${id}`
    })
  })
})

export const { useGetUserQuery } = userApi

이 기능만으로도 대부분의 서버 통신 로직을 간단하게 관리할 수 있습니다.

 

Redux Toolkit(RTK)는 단순한 상태 저장소 이상의 기능을 제공하여, 비동기 요청의 상태 변화를 효율적으로 관리할 수 있습니다.
이 글에서는 RTK의 핵심 기능인 반응형 상태 처리 개념부터 Axios와의 차이점, 그리고 실전 코드 예제까지 한 번에 정리해드립니다.


RTK의 실시간 상태 처리란?

RTK는 다음 세 가지 상태를 자동으로 관리해 줍니다:

  • pending: 요청 중
  • fulfilled: 요청 성공
  • rejected: 요청 실패

이 상태를 기반으로 로딩 스피너, 에러 메시지, UI 갱신 등을 자동화할 수 있어, 아래와 같은 활용이 가능합니다.

 

활용 예시

  1. 채팅 메시지 수신: WebSocket 또는 polling으로 서버에서 메시지를 받아 상태 갱신
  2. 주문 상태 추적: 배송 상태에 따라 UI를 동적으로 변경
  3. 파일 업로드 진행률: 업로드 상태 및 진행률 표시
  4. 알림 센터 구현: 미확인 알림 상태 추적 및 UI 반영

Axios와 RTK 비교

항목 Axios RTK
목적 HTTP 요청 처리 상태관리 + 비동기 통합
상태관리 직접 구현해야 함 자동 제공
로딩/에러 처리 수동 처리 내장 상태로 가능
중앙 상태 공유 Redux 등 필요 Redux store 통합
코드 구조 자유도 높음 공식 구조 권장
DevTools 직접 설정 필요 자동 통합
 

Axios는 네트워크 요청만 처리하고 상태는 별도 관리가 필요합니다.
반면 RTK는 요청과 상태, 흐름을 Redux 아키텍처 안에서 일관되게 처리할 수 있다는 점이 큰 장점입니다.


RTK 사용법 (설정 및 코드 예시)

1. 패키지 설치

npm install @reduxjs/toolkit react-redux

 

2. Slice 생성

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1 },
    decrement: (state) => { state.value -= 1 }
  }
})

export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer
 

3. 비동기 Thunk 생성

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import axios from 'axios'

export const fetchUser = createAsyncThunk('user/fetch', async (userId) => {
  const res = await axios.get(`/api/user/${userId}`)
  return res.data
})

const userSlice = createSlice({
  name: 'user',
  initialState: { data: null, status: 'idle', error: null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => { state.status = 'loading' })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.status = 'succeeded'
        state.data = action.payload
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
  }
})

export default userSlice.reducer
 

4. Store 설정

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counterSlice'
import userReducer from '../features/userSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    user: userReducer
  }
})

5. Provider 설정

 
import { Provider } from 'react-redux'
import { store } from './app/store'

<Provider store={store}>
  <App />
</Provider>

+ Recent posts