이번에 커머스시스템 과제를 진행하면서, 실제 DB를 쓰지 않아서 하나의 MockData를 만들고 데이터를 불러와서 사용하는 식으로사용했습니다.
해당 데이터는 모든 객체에서 공유해야 하기때문에 static으로 선언 해 주었는데, 여기서 문제가 발생했습니다.
문제
상품들을 관리자가 추가 할 수 있는 기능을 만드는데,
제가 처음 확인한 문제는 아래 와같은 불변 객체가 수정 된다고 나온는것을 확인하고.

음..일단 테스트라도 해볼까? 하고 테스트를 해봤지만 역시나, 데이터가 추가 되지 않는것을 확인 하였습니다.
파악
기존 데이터 할당방식을 보면 List.of(...) 로 선언 한 것을 확인 할 수있습니다.
private static final List<Product> PRODUCTS = List.of(
new Product("1", "Galaxy S25", 1_200_000, "최신 안드로이드 스마트폰", 100, CategoryType.ELECTRONIC.getCategoryType()),
... 생략
);
하지만 List는 생각해보면 불변 객체라서 그 안에 데이터 (ex. name , price .. )은 수정할 수 있지만, 리스트 자체의 데이터는 수정 할 수 없습니다.
처음부터 이를 고려하지 않고 작성하여 발생한 이슈라고 볼수 있습니다.
그럼 해당 데이터를 final로 선언했는데 그건 문제가 되지 않는것인가?
-> 네 : final 키워드는 리스트 참조의 재할당만을 방지할 뿐, 리스트 내부 상태나 요소 객체의 변경을 제한하지 않습니다.
그래서 primitive 타입의 경우 값 자체를 직접 보관하므로 값 변경이 불가능하지만, 참조 타입의 경우 final은 참조(주소)만 고정할 뿐 객체의 내부 상태 변경 여부는 상관이 없습니다.
즉, 저기서 문제가 된 객체는 PRODUCTS자체가 아니라 구현체인 LIST가 불변객체이기 때문입니다.
해결(?)
그래서 이제 구현체를 ArrayList로 만들어 주었고, 선언과 동시에 데이터를 할당 할 수있도록 구글링을 해서 초기화를 해주는 방법을 찾아서 해줬습니다.
private static final ArrayList<Product> PRODUCTS = new ArrayList<>(){{
add(new Product("1", "Galaxy S25", 1_200_000, "최신 안드로이드 스마트폰", 100, CategoryType.ELECTRONIC.getCategoryType()));
...생략
}};
그럼 이제 잘 동작하겠지!?

Cannot read field "productRepository" because "this.category" is null

사실 이건 데이터를 불러오는 category를 선언을 안해줘서 그렇다.
이것도 보면 이런 코드를 쓸데없이 구간마다 선언해주고있는데, 이럴 필요없이 객체를 첨에 하나 만들어주고 원하는 데이터만 조회 할 수있게 변경이 필요해 보인다. 일단 지금은 선언하고 테스트 해주면.
category = new Category(CategoryType.ALL);
잘 들어간것을 확인 할 수있습니다
상품 ID = 16 | 상품 이름 = 바겐슈타이거 국자 | 상품 가격 = 13,500 | 잔여 갯수 = 10
상품 ID = 17 | 상품 이름 = tester | 상품 가격 = 100 | 잔여 갯수 = 100

참고(최적화?)
지금 코드를 보면 데이터 선언을 아래와 같은 방식으로 하고있는데 이런방식을 "Double Brace Initialization"라고 한다고 합니다.
private static final ArrayList<Product> PRODUCTS = new ArrayList<>(){{
add(new Product("1", "Galaxy S25", 1_200_000, "최신 안드로이드 스마트폰", 100, CategoryType.ELECTRONIC.getCategoryType()));
... 생략...
}};
근데 이는 내부적으로 ArryList를 상속 받기 위해 "익명 하위 클래스" 가 하나 만들어지고. 그 안에서 add가 이루어 진다고 합니다.
class AnonymousArrayList extends ArrayList<String> { }
class AnonymousArrayList extends ArrayList<String> {
{
add("A");
add("B");
}
}
private static final List<String> list = new AnonymousArrayList();
이런 형태의 코드가 만들어 지는것입니다.
이는 여러 문제를 일으키는데
1. GC가 수거 못하는 메모리 누수 위험 ( 외부 클래스 참조를 몰래 들고있다고 한다.) [이해는 잘 안감..]
2. 불필요한 익명 클래스 생성
3. 직렬화 / 프록시 / 프레임워크 호환성 문제 등이 있을 수 있다고 합니다.
그럼 어떤방식으로 데이터를 할당하는게 좋을까??
Static Initialization Block
Static Initialization Block 이란 클래스가 로딩될 때 단 한번만 실행되어 정적필드를 초기화 하는 코드 블록입니다.
private static final List<String> list = new ArrayList<>();
static {
list.add("A");
list.add("B");
}
이는 클래스 로딩시 딱 한번만 실행 되어. 단순하고 명확하고 추가적인 클래스 생성도 없습니다.
실제로 static 블록을 사용하는것이 더 선호 권장되는 방식이라고 합니다.
그래서 저도 아래와 같은 방식으로 수정 해 주었습니다.
private static final ArrayList<Product> PRODUCTS = new ArrayList<>();
static {
PRODUCTS.add(new Product("1", "Galaxy S25", 1_200_000, "최신 안드로이드 스마트폰", 100, CategoryType.ELECTRONIC.getCategoryType()));
...생략...
}'이슈' 카테고리의 다른 글
| [트러블 슈팅] spring swagger 버전 이슈 ControllerAdviceBean (0) | 2026.02.05 |
|---|---|
| 책임과 역할을 적절하게 분배하지 못한 이슈 (0) | 2026.01.21 |
| [트러블슈팅]java.lang.NullPointerException: Cannot invoke "java.util.List.stream()" because "this.items" is null (1) | 2026.01.21 |
| 제네릭에 extends Number를 해도 사칙연산이 안되는 이유 (0) | 2026.01.14 |
| Next.js 서버컴포넌트(SSR)에서 서버 통신시 쿠키 세팅 안됨이슈 (0) | 2025.12.16 |