관리 메뉴

피터의 개발이야기

[Node.js] Node.js vs Java에서 값 참조와 객체 복사 차이 본문

DevOps/Node.js

[Node.js] Node.js vs Java에서 값 참조와 객체 복사 차이

기록하는 백앤드개발자 2025. 8. 28. 00:12
반응형

ㅁ 들어가며

  Node.js에서의 참조 vs 복사, 그리고 불변성(immutability) — 익숙하게 들리는 개념이지만, Java(Spring) 백엔드 개발자로서 실제로 Node.js 게이트웨이 필터를 변경하면서 참조 전달 특성 때문에 고생한 경험이 있다. 특히 accountId 누락 이슈가 터지고 나서야 이 부분을 제대로 이해하게 되었다. 여기선 왜 문제가 생겼는지, 어떻게 해결했는지, 그리고 재발 방지 설계/테스트 원칙을 정리하였다.

 

 

ㅁ 문제 배경

  • 단위 테스트에선 CustomFilter.refineUser()가 정상적으로 accountId를 세팅했다.
  • 하지만 실제 요청 경로는 CustomFilter.refinedBody()baseBody()AbstractFilter.refineUser()로 흘러가는데, 부모 로직이 accountId 타입을 지원하지 않아서 필드가 싹 사라졌다.
  • 해결: baseBody()가 실행된 이후 자식 필터의 refineUser()를 다시 적용해서 accountId를 복구했다.

 

ㅁ 기본 개념 차이 (Java ↔ Node.js)

ㅇ Java

  • 기본형(primitive): 값 자체가 복사됨 (int, boolean 등)
  • 참조형(reference): 객체의 주소가 전달됨
  • 하지만 실무에선 DTO 매핑/복사, final, String 불변 객체 등 불변 패턴에 익숙하다.
  • 컨트롤러에서 request 객체 자체를 직접 건드는 일은 거의 없다.

ㅇ Node.js

  • 모든 객체가 참조로 전달됨.
    let a = {x:1}; let b = a; b.x = 2; // a.x도 2로 바뀜
  • 함수에 req 객체를 넘기면 원본에 바로 영향을 준다.
  • 얕은 복사(shallow copy)가 많아서 nested object가 원본을 공유할 수 있다.
  • Node.js에선 req, body를 직접 수정하는 일이 흔함.  Java보다 side effect 위험도가 높다.

 

 

ㅁ Java vs Node.js 코드 예시

Java

public void handleRequest(Request req) {
    Request copy = new Request(req); // 새 객체 생성
    copy.setUser("accountId"); 
    // 원래 req는 그대로
}

 

 

Node.js

function handleRequest(req) {
    let copy = req; // 같은 참조
    copy.user = "accountId";
    // 원래 req.user도 바뀌어버림
}

Node.js에서 깊은 복사(deep clone)를 안 하면, 객체를 공유하는 순간부터 수정을 하게 된다.

 

ㅁ 핵심 개념 정리 (Node.js 관점)

  • 참조 전달: JS 객체/배열은 모두 참조
  • 불변성: 입력을 직접 수정하지 않고, 얕은/깊은 복사로 새 객체 만들어 반환
    • 얕은 복사: { ...obj }, Object.assign({}, obj)
    • 깊은 복사: structuredClone(obj), _.cloneDeep(obj)
    • 단, JSON.parse(JSON.stringify(obj))는 Date, Map 등 손실 가능성 있음.
  • 경계에서의 방어적 복사: Router/Service/Filter 진입에서 복사해서 안전한 작업영역을 만든다.

 

ㅁ 경험 사례 요약

  • 원인: 상위 추상화(baseBody)에서 부모의 refineUser()가 먼저 실행되면서 accountId가 증발함
  • 해결: baseBody() 실행 후 자식 필터의 refineUser() 재적용으로 accountId 복원
  • 영향: 기존 aiid, botUserKey엔 영향 없음. 운영 경로에 맞는 통합 테스트 필요성 확인됨

 

ㅁ 안전한 설계 패턴

  • 입력 불변 + 새 객체 반환을 기본 원칙으로
    result.user = { ...user, type: 'accountId' }
  • 공용 헬퍼/추상화는 mutate 금지: 주석/테스트로 안심 보장
  • 재적용(override) 패턴: 공통 구조 만든 다음, 도메인 특수 로직은 자식 필터에서 최종 확정
  • 불가피한 변경은 함수명/주석/테스트로 “mutate” 명확히 알릴 것

 

ㅁ 짧은 실습 예 (참조 수정 vs 새 객체 반환)

let req = { user: { id: "123", type: "accountId" } };
function refineUser_mutate(user) { user.type = "userId"; return user; }     // 원본 변경
function refineUser_clone(user)  { return { ...user, type: "userId" }; }    // 새 객체 반환

console.log("=== mutate ===");
let r1 = refineUser_mutate(req.user);
console.log(req.user, r1); // 둘 다 type: "userId"

console.log("=== clone ===");
req = { user: { id: "123", type: "accountId" } };
let r2 = refineUser_clone(req.user);
console.log(req.user, r2); // 원본은 accountId, 결과는 userId

 

 

ㅁ 백엔드 개발자 관점 take-away

  • Node.js는 참조 전달 특성상, 원본 mutate 위험이 크다. 필터/게이트웨이에선 불변/복사 습관 필수
  • 공통 추상화의 기본 처리 순서가 도메인 특수 로직을 덮어쓰기 쉬우므로, 확정은 자식/도메인 레이어에서 꼭 재적용
  • "테스트는 통과, Dev에서 실패" — 테스트가 운영 경로를 충분히 모사하지 못할 때 자주 터지는 문제

 

ㅁ 학습 단계 제안

  1. 참조 vs 값 복사 차이 실험: 코드 찍어보기
  2. 얕은 복사 vs 깊은 복사 실습
  3. req 객체 수정 여부 실험: 미들웨어에서 req.user 바꿔서 후속 핸들러에서 영향 확인
  4. 불변성 패턴 적용: Redux, FP 방식 Node.js 코드에 도입

 

ㅁ 마무리

  Java에서는 request 객체 직접 수정을 거의 안 해서 이런 묘한 node.js의 side effect 버그로 혼란을 겪었다. 하지만, Node.js에선 참조 전달 때문에 req 수정이 일상인듯 하다. 이번 경험은 그 차이를 명확히 체감한 계기고, 앞으로도 경계지점에서 방어적 복사와 운영 경로 기반 통합 테스트를 습관으로 삼아야겠다.

 

ㅁ 함께 보면 좋은 사이트

[Java] 불변 객체(Immutable Object)란 무엇인가?

 자바스크립트 객체 복제 방법 총정리

 

 

반응형
Comments