Java에서 객체 참조 문제 해결하기: 깊은 복사와 상태 관리

YuJun Oh·2024년 8월 27일
0

문제 상황: 객체 참조로 인한 예상치 못한 데이터 불일치

최근 진행 중인 프로젝트에서 Java 객체 참조로 인해 발생한 예상치 못한 문제를 겪었습니다. 서비스 로직에서 사용자의 라이선스 상태를 업데이트하고 그 변경 사항을 로그로 기록하려고 했지만, 초기 상태와 변경된 상태가 동일하게 출력되는 상황이 발생한 것입니다.

이 문제는 코드상에서 변경 작업이 올바르게 수행되었음에도 불구하고, 로그에 기록된 값들이 변경되기 전의 상태가 아닌 변경된 값으로 출력되는 것이었습니다.

문제의 증상

  1. 초기 상태와 변경된 상태가 동일하게 출력: 로그 기록 시 initialStatechangedState의 값이 동일하게 출력되었습니다. 이로 인해 변경 사항을 추적할 수 없었고, 로그의 신뢰성이 떨어졌습니다.
  2. 해시코드는 다르지만 equals()true 반환: 초기 상태와 변경된 상태의 객체는 서로 다른 해시코드를 가졌지만, equals() 메서드를 통해 비교할 때 true가 반환되었습니다. 이는 두 객체의 내용이 같다는 것을 의미했습니다.

문제의 원인: 객체 참조와 얕은 복사 문제

Java에서는 객체는 참조에 의해 전달되기 때문에, 하나의 객체를 여러 변수에서 참조할 경우 그 객체에 대한 변경이 모든 참조 변수에 영향을 미칩니다. 즉, initialStatechangedState가 동일한 객체를 참조하게 되어, 이후 상태가 변경되면 초기 상태변경된 상태가 동일해지는 문제가 발생한 것입니다.

해결을 위한 시도

문제를 해결하기 위해 다양한 방법을 시도했습니다:

  1. 객체의 얕은 복사(Shallow Copy): 객체를 단순히 복사하여 사용하려 했지만, 여전히 참조 문제가 발생했습니다. 얕은 복사에서는 객체의 참조만 복사되므로, 원본 객체와 복사본이 동일한 데이터를 참조하게 됩니다.

  2. 깊은 복사(Deep Copy) 시도: clone() 메서드와 복사 생성자를 사용해 객체의 깊은 복사를 시도했습니다. 하지만 이 방법은 여전히 문제를 해결하지 못했습니다. clone() 메서드가 객체의 필드 값까지 완전히 복사하지 않는 경우가 있었고, 복사 생성자를 통한 깊은 복사도 불완전하게 구현되었습니다.

  3. 직렬화(Serialization)를 통한 깊은 복사: 객체를 직렬화(Serialization)하고 다시 역직렬화(Deserialization)하여 깊은 복사를 구현하려 했습니다. 하지만 이 방법은 성능에 영향을 주었고, 복잡한 객체 구조에서 적용하기 어려웠습니다.

최종 해결 방법: 깊은 복사를 명확히 구현

최종적으로 문제를 해결한 방법은 깊은 복사를 명확하게 구현하여, 초기 상태를 별도로 보존하는 것이었습니다. 이렇게 하면 이후 변경 작업이 발생하더라도 초기 상태가 변경되지 않고, logUpdate 메서드에서 초기 상태와 변경된 상태를 올바르게 비교할 수 있었습니다.

다음은 실제로 적용한 코드입니다:

public void processLicensesForUpdate(EmrUserUpdateRequestDTO requestDTO, String emrUserId, String requestUserId) {
    // 1. 기존 라이선스 목록의 초기 상태를 깊은 복사하여 보존
    var initialStateCopy = new ArrayList<TblEmrUserLicense>();
    List<TblEmrUserLicense> initialState = findAllByEmrUserId(emrUserId);
    initialState.forEach(a -> initialStateCopy.add(new TblEmrUserLicense(a))); // 깊은 복사 수행

    // 2. 삭제할 라이선스 처리
    deleteLicenses(requestDTO.getDeleteLicenseIds(), initialState);

    // 3. 나머지 요청된 라이선스들 처리 (삭제되지 않은 것들)
    for (EmrUserLicenseSaveRequestDTO requestedLicense : requestDTO.getLicenses()) {
        if (requestDTO.getDeleteLicenseIds().contains(requestedLicense.getUserLicId())) {
            continue; // 3.1 삭제된 라이선스는 처리하지 않음
        }
        updateOrAddLicense(requestedLicense, initialState, requestUserId, emrUserId);
    }

    // 4. 변경된 상태 캡처 후 로그 생성
    List<TblEmrUserLicense> changedState = findAllByEmrUserId(emrUserId);

    logService.logUpdate(initialStateCopy, changedState, requestUserId); // 초기 상태와 변경된 상태 비교
}

왜 이 방법이 효과적이었는가?

이 방법이 문제를 해결한 이유는 깊은 복사를 통해 초기 상태를 별도로 보존했기 때문입니다. 이로 인해, 초기 상태와 변경된 상태가 각각 별도로 유지되었으며, 로그 기록 시 두 상태를 올바르게 비교할 수 있었습니다.

깊은 복사란 무엇인가?

  • 얕은 복사(Shallow Copy)는 객체의 참조만 복사하여, 원본 객체와 복사된 객체가 동일한 참조를 공유하게 만듭니다. 즉, 원본 객체가 변경되면 복사된 객체도 영향을 받게 됩니다.
  • 깊은 복사(Deep Copy)는 객체 자체뿐만 아니라, 그 객체가 참조하는 모든 객체까지 복사하여 완전히 독립된 복사본을 만듭니다. 이를 통해 원본 객체와 복사된 객체가 서로 영향을 미치지 않게 됩니다.

객체 복사 시 유의할 점

  1. 참조형 변수의 복사 주의: 객체 내의 참조형 변수를 복사할 때는, 해당 변수의 내용까지 복사되도록 해야 합니다. 그렇지 않으면 원본 객체의 변경이 복사본에 반영될 수 있습니다.

  2. 객체 상태 캡처 시점 확인: 상태를 캡처하는 시점을 명확히 해야 합니다. 상태 변경 전에 초기 상태를 정확히 캡처하는 것이 중요합니다.

  3. equals()hashCode() 메서드 재정의: 객체를 비교할 때는 equals() 메서드를 제대로 구현해야 합니다. 필드 값을 기준으로 객체를 비교하도록 equals() 메서드를 재정의하는 것이 좋습니다.

결론 및 교훈

이번 문제를 통해, Java에서 객체를 다룰 때 참조복사가 얼마나 중요한지 다시 한번 깨닫게 되었습니다. 특히, 상태 관리가 중요한 상황에서는 객체 참조 문제로 인해 예상치 못한 오류가 발생할 수 있습니다.

이제는 상태 관리가 필요한 모든 상황에서 깊은 복사를 적극적으로 사용하고, 객체 상태를 캡처하는 시점을 명확히 관리할 계획입니다. 여러분도 비슷한 문제를 겪고 있다면, 깊은 복사를 통해 객체 상태를 관리하는 방법을 시도해 보세요!

0개의 댓글

관련 채용 정보