Published on
👁️

메모리 관리 & 복사

Authors
  • avatar
    Name
    River
    Twitter
불변성(Immutability)이란 객체의 상태가 생성된 이후로 변경되지 않는 성질을 의미합니다. 불변 객체는 생성 시점의 상태를 유지하며, 이후에는 그 상태를 변경할 수 없습니다. 불변성을 유지하면 프로그램의 예측 가능성과 안정성이 높아지며, 특히 멀티스레드 환경에서 동시성 문제를 줄일 수 있습니다. 예를 들어, Java에서는 String 객체가 불변 객체로 문자열을 변경하려면 새로운 String 객체를 생성해야 합니다. 실무에서는 Value Object나 불변 컬렉션 등을 활용하여 데이터의 일관성과 안전성을 보장합니다.
상세 설명

불변성(Immutability)이란?

  • 객체의 상태가 생성된 이후로 변경되지 않는 성질
  • 불변 객체는 생성 시점의 상태를 유지하며, 이후에는 그 상태를 변경할 수 없다.
  • 객체의 필드 값이나 내부 데이터가 외부에서 수정될 수 없도록 보장한다.
  • 프로그램의 예측 가능성과 안정성을 높이는 중요한 개념

불변성의 장점

  • 스레드 안전성(Thread Safety)

    • 여러 스레드가 동시에 접근해도 상태가 변경되지 않으므로 안전
    • 별도의 동기화 메커니즘 없이도 스레드 안전성 보장

  • 사이드 이펙트 방지

    • 메서드 호출이나 변수 전달 시 예상치 못한 상태 변경을 방지
    • 함수형 프로그래밍의 핵심 원칙

  • 캐싱과 최적화

    • 객체의 해시코드를 한 번 계산한 후 재사용 가능
    • String Interning(ex. String Pool 활용)과 같은 메모리 최적화 기법 활용 가능

불변 객체 구현

  • Java

    • String 클래스 (대표적인 불변 객체)

      String str1 = "Hello";
      String str2 = str1.concat(" World");
      
      System.out.println(str1);  *// "Hello" (원본 변경 없음)*
      System.out.println(str2);  *// "Hello World" (새로운 객체 생성)*
      
      • String 객체는 한 번 생성되면 내용을 변경할 수 없음
        • 변경 필요 시 Mutable String 클래스StringBuilderStringBuffer 활용
        • StringBuilderStringBufferThread-Safety의 차이
      • 문자열 조작 시 새로운 String 객체가 생성된다.


  • JavaScript

    • Object.freeze()

      const person = Object.freeze({
          name: "John",
          age: 30
      });
      
      person.age = 31;  *// 무시됨 (strict mode에서는 에러)*
      console.log(person.age);  *// 30*
      

    • Immutable.js 라이브러리

      import { Map } from 'immutable';
      
      const map1 = Map({ a: 1, b: 2, c: 3 });
      const map2 = map1.set('b', 50);
      
      console.log(map1.get('b'));  *// 2 (원본 유지)*console.log(map2.get('b'));  *// 50 (새로운 객체)*
      


  • Python

    • 문자열, 튜플, frozenset 등이 기본적으로 불변

      text = "Hello"
      new_text = text + " World"
      print(text)      # "Hello" (원본 유지)
      print(new_text)  # "Hello World" (새 객체)
      
      numbers = (1, 2, 3)  # 튜플은 불변
      # numbers[0] = 5     # 에러! 튜플은 변경 불가
      

    • namedtuple 사용

      from collections import namedtuple
      
      Point = namedtuple('Point', ['x', 'y'])
      p1 = Point(1, 2)
      p2 = p1._replace(x=5)  # 새 객체 생성
      
      print(p1)  # Point(x=1, y=2) (원본 유지)
      print(p2)  # Point(x=5, y=2) (새 객체)
      

    • dataclass(frozen=True)

      from dataclasses import dataclass
      
      @dataclass(frozen=True)
      class ImmutablePerson:
          name: str
          age: int
          
          def with_age(self, new_age):
              return ImmutablePerson(self.name, new_age)
      
      person = ImmutablePerson("Alice", 25)
      older_person = person.with_age(26)
      
      print(person.age)        # 25 (원본 유지)
      print(older_person.age)  # 26 (새 객체)
      
      # person.age = 30  # 에러!
      
      • @dataclass(frozen=True)로 불변 설정
      • 불변 객체(frozen 객체)는 변경 불가

실무에서의 활용

  1. 클래스를 final로 선언
    • 상속을 통한 상태 변경 가능성 차단
  2. 모든 필드를 private final로 선언
    • 외부에서 직접 접근 불가
    • 초기화 후 재할당 불가
  3. Setter 메서드 제공하지 않음
    • 객체 생성 후 상태 변경 방법 차단
  4. 가변 객체 필드의 방어적 복사
    • 생성자에서 입력받은 가변 객체 복사
    • Getter에서 내부 객체의 복사본 반환
  • 쉽게 Lombok의 @Value 애노테이션으로 구현 가능

  • 불변 클래스 직접 구현

    public final class ImmutablePerson {
    
        private final String name;
        private final int age;
        private final List<String> hobbies;
        
        public ImmutablePerson(String name, int age, List<String> hobbies) {
            this.name = name;
            this.age = age;
            *// 방어적 복사: 외부 컬렉션의 변경으로부터 보호*
            this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies));
        }
        
        public String getName() { return name; }
        public int getAge() { return age; }
        
        public List<String> getHobbies() {
            *// 방어적 복사: 내부 상태 노출 방지*
            return new ArrayList<>(hobbies);
        }
    }
    
    • 클래스를 final로 선언하여 상속 방지
    • 모든 필드를 private final로 선언
    • Setter 메서드 제공하지 않음
    • 가변 객체 필드방어적 복사 사용

실무에서의 활용

  • Value Object 패턴

    public final class Money {
        private final BigDecimal amount;
        private final String currency;
        
        public Money(BigDecimal amount, String currency) {
            this.amount = amount;
            this.currency = currency;
        }
        
        public Money add(Money other) {
            if (!this.currency.equals(other.currency)) {
                throw new IllegalArgumentException("Different currencies");
            }
            return new Money(this.amount.add(other.amount), this.currency);
        }
        
        *// equals, hashCode, toString 구현...*
    }
    
    • Value Object

      • 고유 식별자(ID)가 없고, 값 자체로 객체의 동일성을 판단하는 객체
      • 즉, 값이 같으면 같은 객체로 간주
    • DDD(Domain-Driven Design)의 패턴

    • 대개 Value Object는 불변이므로 연산 시 새로운 객체를 반환한다.

    • 이렇게 새로운 객체를 반환하는 이유는 불변성을 유지하고, 100원 + 50원이 150원이 되는 것이지, 100원이 150원으로 변하는 것이 아니라는 것을 보여줄 수 있다.

      즉, VO는 값을 나타내므로, 연산 결과는 새로운 값을 나타내야 한다.


  • 불변 컬렉션

    public final class UserGroup {
        private final List<String> members;
        
        public UserGroup(List<String> members) {
            this.members = Collections.unmodifiableList(new ArrayList<>(members));
        }
        
        public UserGroup addMember(String member) {
            List<String> newMembers = new ArrayList<>(this.members);
            newMembers.add(member);
            return new UserGroup(newMembers);  // 새 객체 반환
        }
    }
    

불변성의 한계와 고려사항

  • 메모리 사용량 증가

    • 객체 변경 시 새로운 객체 생성으로 인한 메모리 오버헤드
    • 특히 큰 객체나 빈번한 변경이 필요한 경우 성능 이슈 가능


  • Builder 패턴 활용

    public final class ImmutableStudent {
        private final String name;
        private final int age;
        private final List<String> subjects;
        
        private ImmutableStudent(Builder builder) {
            this.name = builder.name;
            this.age = builder.age;
            this.subjects = Collections.unmodifiableList(new ArrayList<>(builder.subjects));
        }
        
        public static class Builder {
            private String name;
            private int age;
            private List<String> subjects = new ArrayList<>();
            
            public Builder setName(String name) {
                this.name = name;
                return this;
            }
            
            public Builder setAge(int age) {
                this.age = age;
                return this;
            }
            
            public Builder addSubject(String subject) {
                this.subjects.add(subject);
                return this;
            }
            
            public ImmutableStudent build() {
                return new ImmutableStudent(this);
            }
        }
    }
    
    import lombok.Builder;
    import lombok.Value;
    
    @Value  // 불변 객체 생성 (final, getter, equals/hashCode 자동)
    @Builder
    public class ImmutablePerson {
        String name;
        int age;
        String email;
        List<String> hobbies;
    }
    
    // 사용
    ImmutablePerson person = ImmutablePerson.builder()
        .name("홍길동")
        .age(30)
        .email("hong@example.com")
        .hobbies(Arrays.asList("독서", "영화"))
        .build();
    
    • 불변 객체는 생성 후 변경할 수 없지만, 생성할 때는 복잡할 수 있어서 Builder를 활용한다
    • Builder 클래스는 가변 객체지만, build() 호출 시 불변 객체를 만들어준다.
    • @Builder, @Value 애노테이션으로 불변 객체를 쉽게 정의할 수 있다.

불변성은 프로그램의 안정성과 예측 가능성을 높이는 중요한 원칙이다. 특히 멀티스레드 환경이나 함수형 프로그래밍에서 유용하지만 성능과 메모리 사용량을 고려하여 적절히 활용하는 것이 중요하다.

메모리 최적화 기법

  • 객체 풀링 (Object Pooling)

    • 재사용 가능한 객체들을 미리 생성해서 풀에 보관
  • 지연 초기화 (Lazy Initialization)

    • 실제 필요할 때까지 객체 생성 미루기
  • 플라이웨이트 패턴 (Flyweight Pattern)

    • 공통 데이터를 공유해서 메모리 절약
  • WeakReference 사용

    • 가비지 컬렉션(GC)을 방해하지 않는 약한 참조
  • String Interning

    • String Pool 활용

      (String Pool이란 JVM이 문자열 리터럴을 저장하는 특별한 메모리 영역)

  • 캐싱

    • 계산 결과나 자주 사용되는 객체 재사용

함수형 프로그래밍

  • 함수형 프로그래밍 (Functional Programming)

    • “무엇을 할 것인가”에 집중하는 방식

    • 함수형 프로그래밍의 핵심은 순수 함수로,

      • 같은 입력 ⇒ 같은 출력, 부작용(Side Effect) 없어야 한다.
      • 즉 결과가 이전 호출에 영향을 받지 않아야 한다. (예측 가능)
    • 순수하지 않은 함수

      class Calculator {
          private int total = 0;
          
          public int add(int value) {
              total += value;     // 외부 상태 변경 (부작용!)
              return total;       
          }
      }
      
      Calculator calc = new Calculator();
      System.out.println(calc.add(5));  // 5
      System.out.println(calc.add(5));  // 10
      
    • 순수 함수

      public class PureCalculator {
          public static int add(int current, int value) {
              return current + value;  // 외부 상태 변경 없음, 예측 가능
          }
      }
      
      System.out.println(PureCalculator.add(10, 5));  // 항상 15
      System.out.println(PureCalculator.add(10, 5));  // 항상 15
      

  • 명령형 프로그래밍 (Imperative Programming)

    • 단계 별로 “어떻게 할 것인가”에 집중하는 방식
32Call By Value와 Call By Reference에 대해서 설명해주세요.쉬움
Call By Value와 Call By Reference는 함수 호출 시 인수 전달 방식의 차이를 설명하는 개념입니다. 즉 매개변수와 인수의 관계의 차이입니다. Call By Value는 함수에 인수를 전달할 때, 인수의 실제 값을 복사하여 전달하는 방식으로 함수 내에서 인수의 값을 변경해도 원래 변수에는 영향을 미치지 않습니다. 반면, Call By Reference는 함수에 인수를 전달할 때, 인수의 참조 주소값을 전달하여 값을 변경하는 경우 원본에 영향을 미치게 됩니다. 다만, 배열과 같은 큰 데이터의 경우 주소값만 전달하므로 복사 시간이 효율적일 수 있습니다. Java의 경우 기본적으로 Call By Value이지만 매개변수로 객체를 받는 경우, Call By Reference로 동작합니다.
상세 설명

Call By Value vs Call By Reference 개념

  • Call By Value/Reference는 매개변수와 인수의 관계이다.

  • Call By Value (값에 의한 호출)

    • 함수에 인수의 실제 값을 복사하여 전달
    • 함수 내에서 매개변수 값 변경이 원본 변수에 영향을 주지 않는다
    • 안전하지만 메모리 사용량이 증가할 수 있다


  • Call By Reference (참조에 의한 호출)

    • 함수에 인수의 메모리 주소(참조)를 전달
    • 함수 내에서 매개변수 값 변경이 원본 변수에 직접 영향
    • 메모리 효율적이지만 의도치 않은 부작용(Side Effect) 가능

Call By Value

  • C 언어 예시

    #include <stdio.h>
    
    void modifyValue(int x) {
        x = 100;  // 복사된 값만 변경됨
        printf("함수 내부 x: %d\n", x);  // 100
    }
    
    int main() {
        int num = 50;
        printf("함수 호출 전 num: %d\n", num);  // 50
        
        modifyValue(num);  // num의 값이 복사되어 전달
        
        printf("함수 호출 후 num: %d\n", num);  // 50 (변경되지 않음)
        return 0;
    }
    
    • num의 값 50이 함수의 매개변수 x로 복사됨
    • 함수 내에서 x를 변경해도 원본 num은 영향받지 않음
    • 기본적으로 Call By Value이지만 포인터로 Call By Reference 구현 가능


  • Java 예시

    public class CallByValueExample {
    
        public static void modifyPrimitive(int x) {
            x = 100;
            System.out.println("메서드 내부 x: " + x);  // 100
        }
        
        public static void main(String[] args) {
            int num = 50;
            System.out.println("메서드 호출 전 num: " + num);  // 50
            
            modifyPrimitive(num);
            
            System.out.println("메서드 호출 후 num: " + num);  // 50
        }
    }
    
    • JavaC와 마찬가지로 기본적으로 Call By Value이다.

Call By Reference

  • C++ 포인터 사용

    #include <iostream>using namespace std;
    
    void modifyByPointer(int* ptr) {
        *ptr = 100;  // 포인터가 가리키는 주소의 값 변경
        cout << "함수 내부 *ptr: " << *ptr << endl;  // 100
    }
    
    int main() {
        int num = 50;
        cout << "함수 호출 전 num: " << num << endl;  // 50
        
        modifyByPointer(&num);  // num의 주소를 전달
        
        cout << "함수 호출 후 num: " << num << endl;  // 100 (변경됨)
        return 0;
    }
    
    • C++도 기본적으로 Call By Value이지만 포인터나 참조자를 사용하면 Call By Reference 구현 가능


  • C++ 참조자 사용

    void modifyByReference(int& ref) {
        ref = 200;  // 참조를 통해 원본 값 직접 변경
        cout << "함수 내부 ref: " << ref << endl;  // 200
    }
    
    int main() {
        int num = 50;
        cout << "함수 호출 전 num: " << num << endl;  // 50
        
        modifyByReference(num);  // 참조로 전달
        
        cout << "함수 호출 후 num: " << num << endl;  // 200 (변경됨)
        return 0;
    }
    

Java의 특수한 경우 ⭐

  • Java는 항상 Call By Value이지만 객체의 경우 참조값을 값으로 전달

    class Person {
        String name;
    
        Person(String name) {
            this.name = name;
        }
    }
    
    public class JavaReferenceExample {
        public static void modifyObject(Person p) {
            p.name = "Changed";  // 객체의 내용 변경
            System.out.println("메서드 내부 p.name: " + p.name);  // Changed
        }
        
        public static void reassignObject(Person p) {
            p = new Person("New Person");  // 참조 재할당 (원본에 영향 없음)
            System.out.println("메서드 내부 p.name: " + p.name);  // New Person
        }
        
        public static void main(String[] args) {
            Person person = new Person("Original");
            
            System.out.println("호출 전: " + person.name);  // Original
            modifyObject(person);
            System.out.println("modifyObject 후: " + person.name);  // Changed
            
            reassignObject(person);
            System.out.println("reassignObject 후: " + person.name);  // Changed (여전히)
        }
    }
    
    • Java는 객체는 객체 참조값을 값으로 전달한다. (Call By Value of Reference)
    • 참조를 통해 객체 내용은 변경 가능하지만, 참조 자체의 재할당원본에 영향이 없다.

메모리 관점의 차이

  • Call By Value 메모리 사용

    ┏━━━━━━━━━━━━━━ stack Memory ━━━━━━━━━━━━━━┓
    main()의 스택 프레임:
    ┃   ├── num: 50
    modifyValue()의 스택 프레임:
    ┃   ├── x: 50 (복사된 값)
    ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
    
    • 원본복사본이 별도의 메모리 공간에 저장
    • modifyValue() 함수 종료 시 복사본 메모리 해제


  • Call By Reference 메모리 사용

    ┏━━━━━━━━━━━━━━ stack Memory ━━━━━━━━━━━━━━┓
    main()의 스택 프레임:
    ┃   ├── num: 50 (주소: 0x1000)
    modifyByPointer()의 스택 프레임:
    ┃   ├── ptr: 0x1000 (num의 주소)
    ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
    
    • 주소값만 전달되므로 배열과 같은 큰 데이터의 경우 메모리 효율적
      • 큰 데이터일수록 복사 시간이 많이 걸린다.
    • 원본 데이터에 직접 접근


  • 둘 다 메모리가 2개씩 생기는 것은 맞다. 다만 복사하는 대상이 배열인 경우 Call By Value는 복사 시간이 커져서 비효율적이지만, Call By Reference는 주소값만 전달하므로 효율적이다.

성능 비교

구분Call By ValueCall By Reference
메모리 사용데이터 크기가 크면 많음 (복사본 생성)적음 (주소만 전달)
실행 속도느림 (복사 오버헤드)빠름 (주소 전달만)
안전성높음 (원본 보호)낮음 (의도치 않은 변경 가능)
함수 반환값하나의 값만 반환여러 값 변경 가능
적합한 상황작은 데이터, 안전성 중요큰 데이터, 성능 중요

중요

실무에서는 데이터 크기, 성능 요구사항, 안전성을 고려하여 적절한 전달 방식을 선택해야 한다. 큰 객체는 참조로, 작은 값은 복사로 전달하는 것이 일반적이다.

33방어적 복사에 대해서 설명해주세요.보통
방어적 복사는 객체의 불변성을 보장하기 위해 객체의 복사본을 생성하여 외부로 제공하는 방법입니다. 이는 주로 객체의 내부 상태가 외부에 의해 변경되는 것을 방지하기 위해 사용됩니다. 예를 들어, Java에서 클래스가 불변 객체를 제공하고자 할 때, 해당 객체의 필드가 참조 타입이라면 방어적 복사를 통해 외부에서 필드의 상태를 변경하지 못하도록 합니다. 생성자에서는 입력받은 가변 객체의 복사본을 만들어 저장하고, getter 메서드에서는 내부 객체의 복사본을 반환하여 캡슐화를 보장합니다. 이를 통해 외부에서 반환된 객체를 수정하더라도 원래 객체의 상태는 변하지 않습니다.
상세 설명

방어적 복사(Defensive Copying)란?

  • 객체의 불변성과 캡슐화를 보장하기 위해 객체의 복사본을 생성하는 기법
  • 외부에서 내부 상태를 직접 변경하는 것을 방지
  • 주로 가변 객체(Mutable Object)를 다룰 때 사용
  • 생성자와 접근자 메서드에서 주로 적용
    • 생성자 - 외부에서 전달된 가변 객체는 반드시 복사
    • 접근자 메서드 - 내부 가변 상태를 직접 반환하지 않는다.

방어적 복사가 필요한 이유

  • 문제 상황 예시

    public class BadPeriod {
    
        private final Date start;
        private final Date end;
        
        public BadPeriod(Date start, Date end) {
            this.start = start;  // 위험: 참조 공유
            this.end = end;  // 위험: 참조 공유   
        }
        
        public Date getStart() { return start; } 
        public Date getEnd() { return end; }
    }
    
    // 문제 발생
    Date startDate = new Date();
    Date endDate = new Date(startDate.getTime() + 86400000); // 하루 후
    BadPeriod period = new BadPeriod(startDate, endDate);
    
    // 외부에서 내부 상태 변경 가능!
    startDate.setTime(0);              // 생성자 인수 변경
    period.getEnd().setTime(0);        // getter 반환값 변경
    
    • 방어적 복사가 없는 상황
    • 외부 코드가 객체의 내부 상태를 의도치 않게 변경

방어적 복사 구현

public class SafePeriod {

    private final Date start;
    private final Date end;
    
    public SafePeriod(Date start, Date end) {
        this.start = new Date(start.getTime());  // 생성자에서 방어적 복사
        this.end = new Date(end.getTime());
        
        // 유효성 검사는 복사 후에 수행
        if (this.start.compareTo(this.end) > 0) {
            throw new IllegalArgumentException("시작일이 종료일보다 늦습니다");
        }
    }
    
    public Date getStart() {  // 접근자에서 방어적 복사
        return new Date(start.getTime());
    }
    
    public Date getEnd() { return new Date(end.getTime()); }
}

// 안전한 사용
Date startDate = new Date();
Date endDate = new Date(startDate.getTime() + 86400000);
SafePeriod period = new SafePeriod(startDate, endDate);

startDate.setTime(0);              // 원본 변경해도 영향 없음
period.getEnd().setTime(0);        // 복사본 변경이므로 영향 없음
  • 생성자와 getter 모두에서 방어적 복사 수행

    List<String> maliciousData = new ArrayList<>(Arrays.asList("normal"));
    SafeExample example = new SafeExample(maliciousData);
    
    maliciousData.add("attack1");  // 막힘 ✅
    
    List<String> leaked = example.getItems();
    leaked.add("attack2");  // 막힘 ✅ (복사본이므로)
    
    • 이렇게 양쪽 모두 막지 않는다면 다른 한쪽을 통해 변경할 수 있다.
  • 외부에서 변경 시도해도 내부 상태는 안전

  • 유효성 검사는 복사 후 수행
    • 복사 전 검사 시 TOCTOU 공격 가능

      public class VulnerableClass {
          private final List<String> items;
          
          public VulnerableClass(List<String> items) {
              // 1. 검사 (Check)
              if (items == null || items.isEmpty()) {
                  throw new IllegalArgumentException("Items cannot be null or empty");
              }
              
              // 여기서 다른 스레드가 items를 변경할 수 있음! (시간차 공격)
              
              // 2. 사용 (Use) 
              this.items = new ArrayList<>(items);  // 변경된 데이터로 복사됨!
          }
      }
      

컬렉션에서의 방어적 복사

  • List 방어적 복사

    public class StudentGroup {
        private final List<String> students;
    
        public StudentGroup(List<String> students) {
            this.students = new ArrayList<>(students); // 생성자에서 방어적 복사
        }
    
            public List<String> getStudentsReadOnly() {
                    return Collections.unmodifiableList(new ArrayList<>(students));
            }
            
            public StudentGroup addStudent(String student) {
            List<String> newStudents = new ArrayList<>(this.students);
            newStudents.add(student);
            return new StudentGroup(newStudents);
        }
    

  • Map 방어적 복사

    public class Configuration {
        private final Map<String, String> properties;
        
        public Configuration(Map<String, String> properties) {
            // 방어적 복사 (얕은 복사)
            this.properties = new HashMap<>(properties);
        }
        
        public Map<String, String> getProperties() {
            // 불변 뷰로 반환 (복사 + 읽기 전용)
            return Collections.unmodifiableMap(new HashMap<>(properties));
        }
        
        ...
    }
    
    • Collections.unmodifiableMap()은 읽기 전용 래퍼로 추가 보호를 해준 것이다.

얕은 복사 vs 깊은 복사

  • 얕은 복사 (Shallow Copy)

    public class ShallowCopyExample {
        private final List<List<String>> matrix;
        
        public ShallowCopyExample(List<List<String>> matrix) {
            // 얕은 복사 
            this.matrix = new ArrayList<>(matrix);
        }
        
        public List<List<String>> getMatrix() {
            return new ArrayList<>(matrix);  // 여전히 내부 리스트는 공유됨
        }
    }
    
    • 외부 리스트만 복사, 내부 리스트는 참조 공유

    • 문제 발생 가능

      List<String> row1 = new ArrayList<>(Arrays.asList("a", "b"));
      List<String> row2 = new ArrayList<>(Arrays.asList("c", "d"));
      List<List<String>> matrix = Arrays.asList(row1, row2);
      
      ShallowCopyExample example = new ShallowCopyExample(matrix);
      row1.add("modified");  // 내부 상태 변경됨!
      


  • 깊은 복사 (Deep Copy)

    public class DeepCopyExample {
        private final List<List<String>> matrix;
        
        public DeepCopyExample(List<List<String>> matrix) {
            // 깊은 복사
            this.matrix = new ArrayList<>();
            for (List<String> row : matrix) {
                this.matrix.add(new ArrayList<>(row));
            }
        }
        
        public List<List<String>> getMatrix() {
            List<List<String>> copy = new ArrayList<>();
            for (List<String> row : matrix) {
                copy.add(new ArrayList<>(row));
            }
            return copy;
        }
    }
    
    • 모든 레벨의 객체를 복사

다양한 최적화 기법

  • 방어적 복사

    public class PerformanceConsideration {
        private final byte[] largeData;
        
        public byte[] getData() {
            return largeData.clone();
        }
    
    • clone()은 얕은 복사 메서드 (원시 타입의 경우 완전한 복사)
    • 안전하지만 비용 높음


  • 읽기 전용 뷰 (방어적 뷰)

    public class PerformanceConsideration {
        private final byte[] largeData;
        
        ...
        
        public ByteBuffer getDataAsReadOnly() {
            return ByteBuffer.wrap(largeData).asReadOnlyBuffer();
        }
    }
    
    • 복사하지 않고 원본 데이터를 래핑만 한다.
    • 메모리 절약


  • 지연 복사 (Lazy Copying)

    public class LazyCopyExample {
        private final List<String> originalData;
        private List<String> cachedCopy;
        
        public LazyCopyExample(List<String> data) {
            this.originalData = new ArrayList<>(data);
        }
        
        public List<String> getData() {
            if (cachedCopy == null) {
                cachedCopy = Collections.unmodifiableList(new ArrayList<>(originalData));
            }
            return cachedCopy;
        }
    }
    
    • 캐싱 최적화
      • 첫 호출 시에만 복사 작업 수행, 이후엔 캐시된 결과 재사용
    • 실제 필요할 때까지 복사를 미루는 최적화 기법

주의

방어적 복사는 안전성을 보장하지만 성능 비용이 발생합니다. 객체의 크기와 사용 패턴을 고려하여 적절한 전략을 선택해야 합니다. 때로는 불변 컬렉션이나 읽기 전용 뷰가 더 효율적일 수 있습니다.

34자바스크립트의 메모리 관리에 대해서 아는대로 설명해주세요.어려움
JavaScript의 메모리 관리는 주로 가비지 컬렉션을 통해 이루어집니다. JavaScript는 자동 메모리 관리 언어로, 개발자가 명시적으로 메모리를 할당하고 해제할 필요가 없습니다. 가비지 컬렉터는 더 이상 참조되지 않는 객체를 탐지하고 메모리를 해제합니다. JavaScript 엔진인 V8 엔진은 주로 'Mark-and-Sweep' 알고리즘을 사용하여 메모리를 관리하는데 이 알고리즘은 객체 참조 관계를 탐색하여 도달 가능한 객체를 표시하고, 도달할 수 없는 객체를 수집하여 메모리를 해제합니다. 자동으로 해제되지 않는 경우도 있기 때문에 실무에서는 메모리 누수를 방지하기 위해서 전역 변수 사용을 최소화하고, 이벤트 리스너와 타이머 등을 사용 후 적절히 해제하며, 클로저 사용 시 주의해야 합니다.
상세 설명

JavaScript 메모리 관리 개요

  • JavaScript는 자동 메모리 관리 언어
  • 개발자가 명시적으로 메모리 할당/해제를 하지 않음
  • 가비지 컬렉션(Garbage Collection)을 통한 자동 메모리 회수
  • 주로 Mark-and-Sweep 알고리즘 사용

메모리 할당과 생명주기

  • 메모리 할당

    // 1. 값 할당
    let number = 42;               // 스택에 저장
    let string = "Hello World";    // 힙에 저장
    
    
    // 2. 객체 할당
    let object = {                 // 힙에 저장
            name: "JavaScript",
        version: "ES2023"
    };
    
    
    // 3. 함수 할당
    let func = function() {        // 힙에 저장
            return "Hello";
    };
    
    
    // 4. 배열 할당
    let array = [1, 2, 3, 4, 5];   // 힙에 저장
    
    Stack Memory
    ├── num: 42                   // 원시타입 - 값 직접 저장
    ├── str: 0x1000               // 참조타입 - 힙 주소 저장
    ├── list: 0x2000              // 참조타입 - 힙 주소 저장
    ├── localInt: 100             // 원시타입 - 값 직접 저장
    ├── localStr: 0x3000          // 참조타입 - 힙 주소 저장
    └── localList: 0x4000         // 참조타입 - 힙 주소 저장
    
    Heap Memory
    ├── 0x1000: String "Hello" 객체
    ├── 0x2000: ArrayList 객체
    ├── 0x3000: String "Hello" 객체 (또는 String Pool 참조)
    └── 0x4000: ArrayList 객체
    
    • 원시 타입
      • 스택 메모리에 직접 저장
    • 참조 타입
      • 힙 메모리에 저장, 스택에는 참조값 저장


  • 메모리 생명주기

    function createObjects() {
        // 1. 메모리 할당
        let localVar = { data: "temporary" };
        let arr = new Array(1000).fill(0);
        
        // 2. 메모리 사용
        localVar.data = "modified";
        arr[0] = 100;
        
        // 3. 함수 종료 시 자동으로 가비지 컬렉션 대상이 됨
        return localVar.data; 
    }
    
    createObjects();
    
    • 함수 종료 후 자동으로 해제됨 (GC)

가비지 컬렉션 알고리즘

  • Mark-and-Sweep 알고리즘
    let globalVar = { id: 1 };
    
    function example() {
        let localVar = { id: 2 };
        let circularRef = { id: 3 };
        
        // 순환 참조 생성
        circularRef.self = circularRef;
        localVar.circular = circularRef;
        
        // 함수 종료 후 Mark-and-Sweep 동작
        
        return localVar;
    }
    
    
    
    let result = example();   // result가 참조하는 객체들은 아직 살아있다
    
    result = null;   // 이제 모든 객체들이 가비지 컬렉션 대상
    
    • 함수 종료 후 Mark-and-Sweep 동작
      1. Mark : 루트에서 도달 가능한 객체 표시
      2. 루트(전역변수, 스택)에서 obj1, obj2에 도달 불가능
        • 루트 객체들 (전역 변수, 함수 매개변수 등)
      3. Sweep : 표시되지 않은 obj1, obj2 모두 해제!
    • Mark 단계
      • 루트 객체부터 시작하여 도달 가능한 모든 객체 표시
    • Sweep 단계
      • 표시되지 않은 객체들의 메모리 해제


  • Reference Counting (구식 방법)

    function oldGCProblem() {
        let obj1 = {};
        let obj2 = {};
        
        obj1.ref = obj2;  // obj2의 참조 카운트 +1
        obj2.ref = obj1;  // obj1의 참조 카운트 +1
    }
    
    • 함수 종료 후에도 참조 카운트가 둘 다 0이 되지 않아 메모리 누수 발생
      • 현대 브라우저는 Mark-and-Sweep으로 이 문제 해결

V8 엔진의 메모리 관리

  • 힙 구조

    V8 Memory Layout
    
    ┌──────────────────────┐
    Young Generation   │  ← 새로 생성된 객체들
    │  ┌────────────────┐  │
    │  │   Eden Space   │  │  ← 최초 할당 공간
    │  └────────────────┘  │
    │  ┌────────────────┐  │
    │  │ Survivor Space │  │  ← 첫 번째 GC 생존 객체
    │  └────────────────┘  │
    └──────────────────────┘
    ┌──────────────────────┐
    Old Generation    │  ← 오래 살아남은 객체들
    │                      │
    └──────────────────────┘
    


  • 세대별 가비지 컬렉션

    • Young Generation (빠른 GC)

      function createTemporaryObjects() {
          for (let i = 0; i < 1000; i++) {
              let temp = { index: i, data: new Array(100) };
              // 대부분의 temp 객체들은 Young Generation에서 정리됨
          }
      }
      

    • Old Generation (느린 GC)

      let persistentCache = new Map();  // 오래 살아남는 객체
      function addToCache(key, value) {
          persistentCache.set(key, value);
          // 이 객체들은 Old Generation으로 이동
      }
      

  • V8 엔진
    • Google이 개발한 JavaScript 엔진
    • 즉, JavaScript 코드를 실행하는 프로그램
    • Chrome 브라우저, Node.js, Electron 등에서 사용된다.

메모리 누수 패턴과 해결책

  1. 전역 변수로 인한 누수

    • 문제 코드

      function badFunction() {
          // 암묵적 전역 변수 생성
          leakyVariable = "이 변수는 전역이 됩니다";  
          
          // 명시적 전역 변수
          window.anotherLeak = { data: "큰 객체" };
      }
      
      • var, let, const 없음 ⇒ 자동으로 전역 변수 생성

      • JavaScript 엔진의 내부 동작

        1. 현재 스코프에서 leakyVariable 찾기
        2. 없으면 상위 스코프에서 찾기
        3. 전역 스코프까지 가도 없으면...
        4. 전역 객체(window)에 새로 생성!
        
    • 해결책

      function goodFunction() {
          "use strict";  // strict mode 사용
          let localVariable = "지역 변수로 사용";
          
          if (typeof window.myNamespace === 'undefined') {
              window.myNamespace = {};  // 필요시 전역 사용
          }
      }
      
      • strict mode 적용 효과
        • 선언되지 않은 변수에 할당 시 에러 발생
        • 실수로 전역 변수 생성하는 것을 방지


  2. 이벤트 리스너 누수

    • 문제 코드

      function badEventHandling() {
          let element = document.getElementById('button');
          let largeData = new Array(1000000).fill('data');
          
          element.addEventListener('click', function() {
              console.log(largeData.length);  // largeData가 메모리에 계속 남음
          });
      }
      
      • largeData는 객체로 GC의 제거 대상인데 이벤트가 참조하고 있어서 제거할 수 없다.
        • 리스너 제거하지 않음 ⇒ 메모리 누수!
      • element는 DOM 요소에 대한 참조로, 브라우저 메모리에 있는 것으로 대상이 아니다.
    • 해결책

      function goodEventHandling() {
          let element = document.getElementById('button');
          let largeData = new Array(1000000).fill('data');
          
          function clickHandler() {
              console.log(largeData.length);
          }
          
          element.addEventListener('click', clickHandler);
          
          function cleanup() {
              element.removeEventListener('click', clickHandler);
              largeData = null;
          }
          
          return cleanup;   // 적절한 시점에 cleanup 호출
      }
      
      • 적절하게 이벤트 리스너를 제거해준다.


  3. 클로저로 인한 누수

    • 문제 코드

      function createClosure() {
          let largeArray = new Array(1000000).fill('data');  // 1MB 배열
          let smallData = 'small';
          
          return function() {
              return smallData;
          };
      }
      
      • 코드 상에서 smallData만 사용하여 largeArrayGC가 제거할 것으로 예상
        • largeArray도 함께 유지됨
      • 클로저는 inner 함수 + 그 함수가 접근할 수 있는 외부 변수들
        • 클로저 함수가 smallData만 써도 클로저 스코프에는 smallData, largeArray가 둘 다 들어간다.
        • 이는 JS 엔진의 판단으로 나중에 필요할 수도 있으니 일단 보관하는 것이다.
    • 해결책 1

      function createOptimizedClosure() {
          let largeArray = new Array(1000000).fill('data');
          let smallData = 'small';
          
          let extractedData = smallData;      // 필요한 데이터만 추출
          largeArray = null;  // 명시적으로 해제
          
          return function() {
              return extractedData;
          };
      }
      
      • 필요한 데이터만 클로저에 포함

    • 해결책2

      const privateData = new WeakMap();
      
      function createWithWeakMap() {
          let obj = {};  // Key 역할
          privateData.set(obj, 'small data');
          
          return {
              getData() {
                  return privateData.get(obj);
              },
              destroy() {
                  privateData.delete(obj);
                  obj = null;
              }
          };
      }
      
      • WeakMap 사용
        • 클로저 스코프에 큰 데이터를 저장하지 않음
        • 필요할 때만 WeakMap에서 조회
        • destroy() 호출 시 완전히 정리됨


  4. 타이머로 인한 누수

    • 문제 코드

      function badTimer() {
          let largeData = new Array(1000000).fill('data');
          
          setInterval(function() {
              if (document.getElementById('status')) {
                  // largeData가 계속 참조됨
                  console.log('Status updated', largeData.length);
              }
          }, 1000);
      }
      
      • 타이머를 정리하지 않아서 생기는 메모리 누수
    • 해결책

      function goodTimer() {
          let largeData = new Array(1000000).fill('data');
          
          let intervalId = setInterval(function() {
              let statusElement = document.getElementById('status');
              if (statusElement) {
                  console.log('Status updated');
              } else {
                  // 요소가 없으면 타이머 정리
                  clearInterval(intervalId);
                  largeData = null;
              }
          }, 1000);
          
          // 정리 함수 반환
          return function cleanup() {
              clearInterval(intervalId);
              largeData = null;
          };
      }
      

메모리 모니터링과 디버깅

  • Chrome DevTools 활용

    • 메모리 사용량 측정

      function measureMemory() {
          if ('memory' in performance) {
              const memory = performance.memory;
              console.log('Used JS Heap Size:', memory.usedJSHeapSize);
              console.log('Total JS Heap Size:', memory.totalJSHeapSize);
              console.log('JS Heap Size Limit:', memory.jsHeapSizeLimit);
          }
      }
      

    • 메모리 누수 테스트

      function testMemoryLeak() {
          const before = performance.memory.usedJSHeapSize;
          
          // 의심스러운 코드 실행
          for (let i = 0; i < 1000; i++) {
              createSuspiciousObject();
          }
          
          // 강제 가비지 컬렉션 (Chrome DevTools에서만 가능)
          if (window.gc) {
              window.gc();
          }
          
          const after = performance.memory.usedJSHeapSize;
          console.log('Memory increase:', after - before);
      }
      


  • WeakMap과 WeakSet 활용

    • 강한 참조 (메모리 누수 가능) : Map

      const cache = new Map();
      
      function badCaching(element) {
          cache.set(element, expensiveComputation(element));
      }
      
      • element가 DOM에서 제거되어도 캐시에 남아있는다.

    • 약한 참조 (자동 정리) : WeakMap

      const weakCache = new WeakMap();
      
      function goodCaching(element) {
          weakCache.set(element, expensiveComputation(element));
      }
      
      function expensiveComputation(element) {
          return { computed: 'expensive result' };
      }
      
      • element가 DOM에서 제거되면 캐시에서도 자동 제거

중요

JavaScript의 메모리 관리는 대부분 자동으로 이루어지지만, 개발자가 메모리 누수를 방지하고 성능을 최적화하기 위해 주의해야 할 패턴들이 있다. 특히 SPA(Single Page Application)에서는 컴포넌트 생명주기 관리가 매우 중요하다.