16 분 소요

AddresssBook 클래스 구현을 시작하며

Java를 이용해 주소록(AddressBook)프로그램을 만들 때 이전 글에서 개인의 데이터를 가지고 있는 entity역할을 하는 Personal(개인)클래스를 구현에 대한 설명을 하였습니다.

이번 시간에는 Personal(Entity)를 관리할 Control역할을 하는 AddressBook 클래스를 구현하려 합니다.

AddressBook 클래스를 구현하면서 ArrayList의 얕은 복사, 깊은 복사에 대한 설명하려고 합니다.

프로젝트에 대한 설명없이 바로 ArrayList의 얕은 복사와 깊은 복사에 대해 설명하려면 간단한 예시로 밖에 설명을 못하는데, 이는 이미 많은 블로그나 책에서 하고 있기 때문에 굳이 제가 또 해야할 필요는 없다고 생각합니다.

대신에 저는 상대적으로 조금 더 복잡한 프로그램을 구현하면서 이를 설명하려고 합니다.

이렇게 프로그램을 구현하면서 개념을 적용하면 더 깊은 이해를 할 수 있을 것입니다.

그럼 이제 AddressBook 클래스를 구현하면서 ArrayList의 얕은 복사와 깊은 복사에 대해 설명하도록 하겠습니다.

필드

ArrayList를 활용해 자료형이 ArrayList<Personal>인 personals가 Personal 객체들을 관리합니다.

    //인스턴스 멤버(필드)
    private ArrayList<Personal> personals;

생성자

생성자는 매개변수가 없는 디폴트생성자가 있습니다.

디폴트생성자의 경우 Personal을 배열요소로 하는 ArrayList의 객체를 새로 생성해서 personals에 그 주소를 저장합니다.

 //디폴트생성자
    public AddressBook()
    {
        this.personals = new ArrayList<Personal>();
    }

getter 메소드

필드멤버가 private이기 때문에 외부에서 ArrayList의 length만큼 반복구조를 이용할 때 그 값에 접근하기 위해 getter메소드가 필요합니다.

    //personals 가져오기
    public ArrayList<Personal> getPersonals()
    {
        return this.personals;
    }

record 메소드

AddressBook에 새로운 Personal을 저장하기 위한 도메인 용어로 Record(기재하기)를 선택하였습니다.

name, address, telephoneNumber, emailAddress를 매개변수로 입력 받아서 이를 이용하여 Persoanl 객체를 생성한 다음에 AddressBook의 ArrayList<Personal>의 배열요소에 저장하고, 그 위치를 반환합니다.

기재하기가 실패할 경우 -1을 반환합니다.

//Personal 객체 정보 기재하기
public  int record(String name, String address,
    String telephoneNumber, String emailAddress)
{
    int index = -1;//기재하기 실패할 경우 -1반환
    //Personal객체생성
    Personal personal = new Personal(name, address,
     telephoneNumber, emailAddress);
    //Personal객체 추가하기
    boolean doesRecordingSucceed = this.personals.add(personal);
    if(doesRecordingSucceed == true)
    {
        //새로 추가한 Personal객체의 배열첨자알아내기
        index = this.personals.indexOf(personal);
    }
    //위치를 출력한다.
    return index;
}

getAt 메소드

배열의 위치 정보(배열첨자)를 입력하면 거기에 해당하는 Personal 객체가 반환됩니다.

    //Personal 객체 정보 가져오기
    public Personal getAt(int index)
    {
        return this.personals.get(index);
    }

find 메소드

이름으로 주소록에서 원하는 사람의 정보를 찾는 메소드입니다.

주소록에서 개인의 정보를 찾기 위해 주소, 전화번호, 이메일주소로 검색을 하는 것은 부자연스럽다고 생각합니다.

보통은 원하는 정보(주소, 전화번호, 이메일주소)를 얻기 위해 이름으로 검색을 하는게 자연스럽다고 생각하여 find는 name을 매개변수로 하도록 정의했습니다.

이름으로 검색할 경우 동명이인이 있을 수도 있기 때문에 반환값은 Personal의 위치들을 저장한 위치 배열(int[] index)을 반환합니다.

위치배열을 만들 때 문제가 배열은 생성할 때 그 크기를 알아야만 생성할 수 있기 때문에 ArrayList에서 입력 받은 name과 같은 이름이 있는지 확인하여 같은 이름의 개수(count)를 셉니다.

이 후 count크기 만큼의 배열을 할당하고(count = 0일 경우 null반환), 다시 ArrayList의 처음부터 마지막까지 name과 같은 이름이 있는지 비교하면서 같은 경우 그 위치를 위치배열(int[] indexes)에 저장한 뒤에 위치배열을 반환합니다.

//name 으로 Personal 객체 찾기
public int[] find(String name)
{
    //배열값 초기화
    int[] indexes = null;
    int count = 0;//찾은 개수 초기화
    //personals의 처음부터 끝까지 찾고자 하는 이름과
    // 비교하여 찾은 개수를 센다.
    for (Personal personal: this.personals)
    {
        if(name.compareTo(personal.getName())==0)
        {
            //찾은 개수를 증가시킨다.
            count++;
        }
    }
    //찾은게 있으면
    if(count > 0)
    {
        //배열을 할당한다.
        indexes = new int[count];
    }
    //personals의 다시 처음부터 끝까지 반복을 돌리면서
    // 같은 배열요소의 위치를 저장한다.
    int index = 0;//배열첨자
    Personal personal =null;
    for(int i = 0; i < this.personals.size(); i++)
    {
        personal = this.personals.get(i);
        if(name.compareTo(personal.getName())==0)
        {
            //위치를 저장한다.
            indexes[index] = i;
            //배열첨자를 증가시킨다.
            index++;
        }
    }
    return indexes;
}

find에 ArrayList활용하기

위처럼 배열을 사용하면 같은 이름을 찾기 위한 행위를 2번 하기 때문에 비효율적입니다.

그래서 차라리 int[] 배열을 이용하는 것보다는 차라리 여기에도 ArrayList<Integer>를 이용하는 것이 훨씬 낫습니다.

ArrayList의 경우 크기를 유동적으로 변경할 수 있기 때문에 같은 이름을 찾기 위한 행위를 한 번만 수행하면 됩니다.

//name 으로 Personal 객체 찾기
public ArrayList<Integer> find(String name)
{
    //name으로 찾은 Personal 객체들의 위치를
    // 저장할 위치ArrayList 생성하기
    ArrayList<Integer> indexes = new ArrayList<>();
    //personals의 처음부터 끝까지 반복을 돌리면서
    // name과 같은 배열요소의 위치를 저장한다.
    Personal personal =null;
    for(int i = 0; i < this.personals.size(); i++)
    {
        //해당 위치의 Personal객체를 구한다.
        personal = this.personals.get(i);
        //해당 Personal의 name이 찾고자 하는 name과 같으면
        if(name.compareTo(personal.getName())==0)
        {
            //위치를 저장한다.
            indexes.add(i);
        }
    }
    return indexes;
}

correct 메소드

correct 메소드는 변경을 원하는 Personal객체의 배열 위치와 변경 내용(주소, 전화번호, 이메일주소)를 매개변수로 입력받습니다.

이후 입력받은 index를 활용해 주소록의 ArrayList<Personal>에서 해당 personal의 참조 주소값을 얻은 뒤 그 주소를 바탕으로 하여 Personal의 setter메소드를 이용해 personal의 내용을 변경합니다.

이후에 다시 내용만 변경되고 같은 객체인 Personal을 ArrayList의 set 메소드를 통해 element에 다시 끼웁니다.

그리고 마지막으로 변경한 Personal의 위치를 다시 반환합니다.

//Personal 객체 정보 수정하기(주소, 전화번호,
// 이메일주소 변경 가능, 이름은 변경 불가)
public int correct(int index, String address,
 String telephoneNumber, String emailAddress)
{
    //기존의 personal 객체의 참조값을 얻는다.
    Personal personal = this.personals.get(index);
    //기존의 personal의 내용을 변경한다.
    personal.setAddress(address);
    personal.setTelephoneNumber(telephoneNumber);
    personal.setEmailAddress(emailAddress);
    //내용만 변경된 기존의 personal을 다시 끼운다.
    this.personals.set(index, personal);//여기선 필요없음
    //수정한 personal객체를 이용해 배열위치를 알아낸다.
    index = this.personals.indexOf(personal);
    //배열 위치를 출력한다.
    return  index;
}

그런데 여기서 갑자기 ‘personal 객체를 새로 생성한 것이 아닌데 굳이 personal을 set을 통해 끼울 필요가 있을까?’라는 의문이 들었습니다.

그리고 ‘실제로 그럼 저 ArrayList의 set의 반환값인 personal과 기존에 index 위치에 있는 personal과 같은 객체인가?’ 라는 의문이 들어 실제로 이의 주소값을 비교해보니 같은 객체라는 사실을 알 수 있었습니다.

즉, this.personals.set(index, personal); 이 코드는 전혀 필요가 없다는 뜻입니다.

그냥 해당 위치(index)의 Personal객체로 접근하여 내용만 변경해주면 되지 이를 다시 끼울 필요가 없습니다.

correct 구현시 다른 방법

이 때 입력받은 매개변수(주소, 전화번호, 이메일주소)를 바탕으로 setter를 이용하지 않고, Personal의 매개변수를 가지는 생성자를 활용해 새로운 Personal 객체를 생성하는 방법도 있습니다.

대신 이 방법에서는 새로 생성한 객체를 삽입해야하기 때문에 this.personals.set(index, personal); 이 꼭 필요합니다.

public int correct(int index, String address,
 String telephoneNumber, String emailAddress)
{
    //새로운 Personal객체를 생성함
    Personal personal = new Personal(this.personals.get(index).getName(),
        address, telephoneNumber, emailAddress);
    this.personals.set(index, personal);//여기선 꼭 필요함.
    index = this.personals.indexOf(personal);
    return  index;
}

저는 여기서 주소록에서 사람의 내용을 수정할 때 기존 사람은 그대로고 정보만 수정한다는 점을 착안하여 새로운 객체(사람)를 생성하여 끼워넣는 후자의 방법보다는 기존의 객체에 내용만 변경하는 전자의 방법을 택하였습니다.

erase 메소드

erase메소드는 지울 위치를 입력받아서 ArrayList의 remove(int index)메소드를 활용해 지울 객체(Personal)를 꺼내어 이를 반환합니다.

이 꺼낸 Perosnal이 null이 아니면 성공적으로 꺼낸것이기 때문에 배열에서 지워졌다는 의미로 index에 -1을 대입한 뒤에 이를 출력합니다.

//Erase
public int erase(int index)
{
    Personal personal = this.personals.remove(index);
    if(personal != null)
    {
        index = -1;
    }
    return index;
}

arrange 메소드

정렬하기를 사용하기 위해 Collections 클래스의 static메소드인 sort를 호출합니다. 이 때 매개변수로 비교할 기준을 제시해줘야 하는데 Comparator<Personal>을 익명클래스로 하여 매개변수로 대입합니다.

그리고 익명클래스 내부에서 compare를 오버라이딩해야 합니다.

이 때 compare의 매개변수가 Personal자료형 2개이고, 이름을 기준으로 오름차순으로 정렬을 하려고 하기 때문에 각 Personal에서 getName()메소드를 통해 name을 구하고, name의 자료형은 String이기 때문에 String의 compareTo메소드를 이용하여 name끼리 비교한 결과(비교대상보다 크면 1, 같으면 0, 작으면 -1)을 반환하도록 합니다.

//Arrange
public void arrange()
{
    Collections.sort(this.personals, new Comparator<Personal>() {
        @Override
        public int compare(Personal one, Personal other)
        {
            return one.getName().compareTo(other.getName());
        }
    });
}

AddressBook의 print메소드 구현하기

앞으로 있을 메인테스트를 위해서는 출력이 빈번할 것입니다.

그래서 출력을 위한 코드가 중복이 될텐테, 이 중복을 최소화 하기 위해 AddressBook의 메소드로 print함수를 구현합니다.

printAddressBook 코드

주소록에 있는 전체 Personal객체의 정보를 순차적으로 출력하는 메소드입니다.

여기에 매개변수로 입력 받는 number를 통해 지금 몇번째 테스트인지를 구분할 수 있습니다.

//AddressBook에 기재된 전체 Personal을 순차적으로 출력하기
public void printAddressBook(int number)
{
    Personal personal = null;
    int index = 0;
    while(index < this.personals.size())
    {
        personal = this.getAt(index);
        System.out.printf(" %d.%d", number, index + 1).println(personal);
        index++;
    }
}

printf의 경우 반환값이 PrintStream이기 때문에 .을 이용해 바로 메소드를 이용할 수 있습니다.

그래서 System.out.printf(“ %d.%d”, number, index + 1).println(personal); 표현이 가능합니다.

여기에 이전 글에서 이미 구현한 Personal의 toString메소드를 이용합니다.

@Override
public String toString()
{
    return new String(this.name + ", " + this.address + ", " + this.telephoneNumber + ", "
                + this.emailAddress);
}

그래서 println에는 personal객체만 넣어주면 자동으로 toString이 붙어서 personal의 필드값들이 ,와 합쳐진 String클래스가 생성되고, 이를 println을 통해 콘솔창에 출력해줍니다.

clone 메소드(얕은 복사)

이제 clone에서 테스트할 때 이용할 모든 메소드들이 구현되었으니 드디어 이 글에서 제일 중요한 clone에 대해서 설명하도록 하겠습니다.

public class AddressBook  implements Cloneable
{
    //얕은 복사
    @Override
    public AddressBook clone() throws CloneNotSupportedException
    {
        AddressBook addressBook = new AddressBook();
        addressBook.personals = (ArrayList<Personal>)this.personals.clone();
        return addressBook;
    }
}

위의 코드를 보시면 새로운 AddressBook객체를 생성한 다음에 그 멤버인 personals는 this.personals의 clone메소드의 반환값을 받은 다음에 addressBook을 return합니다.

personals의 자료형은 ArrayList인데 ArrayList는 Object의 clone메소드를 오버라이딩하였습니다.

자바공식 문서에서 찾아 본 결과 ArrayList에서 정의해놓은 clone의 코드는 아래와 같습니다.

ArrayList의 clone메소드

public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }                
}

ArrayList의 코드를 찾아보니 AbstractList클래스를 상속받고 List인터페이스를 구현하고 있으며, Cloneable인터페이스를 구현함으로써 clone을 오버라이딩했다는 것을 명시해놨습니다.

그래서 ArrayList는 위의 내용으로 clone을 재정의한 것입니다.

ArrayList에서 super는 누구?

clone의 코드 내부에서 super는 당연히 AbstractList를 가리킵니다.
왜냐하면 인터페이스의 경우 다중상속이 가능하기 때문에 구분을 위해서 반드시 super앞에 "인터페이스명."을 붙여 줘야 합니다.

하지만 클래스의 경우 단일 상속만 받을 수 있기 때문에 super만 붙은 경우는 부모클래스를 가리킵니다.

ArrayList의 부모클래스인 AbstractList를 오라클 공식문서에서 찾아보면 **clone inherited from class java.lang.Object**를 확인할 수 있습니다.

얕은 복사

즉, 결국에 super.clone을 호출하면 Object의 clone을 호출한 것과 같은 결과입니다.

Object의 clone의 경우 새로운 ArrayList<Personal>객체를 생성한 다음에 그 필드에 대해서는 단순히 원본의 값을 복사하는데 그칩니다.

이 때, 복사하는 객체의 필드가 기본 자료형 또는 String 가지고 있는 경우는 얕은 복사니 깊은 복사니 하는 문제가 발생하지 않습니다.

즉, 복사본의 값을 변경하여도 원본에는 전혀 지장이 없습니다.

물론 String도 클래스이지만 String의 특성상 값을 변경하여도 원본은 바뀌지 않습니다.

이에 대해서는 이전 글 Personal을 구현하면서 String의 특성을 자세하게 설명했으니 참고 부탁드립니다.

Java프로젝트하면서 String의 특성으로 인한 깊은 복사 설명하기

하지만 ArrayList는 String이나 기본자료형을 배열요소로 가지고 있는게 아니라 Personal이라는 객체들을 배열요소로 가지고 있습니다.

즉, 각 배열요소는 Personal을 참조하는 주소값입니다.

이 경우에 Object의 clone을 그대로 이용하면 Personal을 참조하는 주소값을 그대로 복사하기 때문에 얕은 복사가 됩니다.

얕은 복사 테스트

얕은 복사를 테스트하기 위해 originalAddressBook이라는 객체를 생성해서 여기에 6명의 Personal의 정보를 record한 뒤에 이를 ArrayList의 처음부터 끝까지 반복구조를 통해 출력해봅니다.

그리고 아까 구현했던 Personal객체의 toString과 printAddressBook을 이용하여 코드 중복을 최소화합니다.

public static void main(String[] args)
{
    System.out.println("AddressBook 메인테스트");
    //1. AddressBook 기본생성자 호출
    AddressBook originalAddressBook = new AddressBook();
    System.out.println("1. AddressBook 기본생성자 호출 : " 
    + originalAddressBook.getPersonals());
    //2. 주소록에 홍길동, 서울시 중구, 021766710, Hong@naver.com 을 기재한다.
    int index = originalAddressBook.record("홍길동", "서울시 중구",
            "021766710", "Hong@naver.com");
    System.out.printf("2. 주소록에 Record한 내용 GetAt : " ).println
            (originalAddressBook.getAt(index));
    //3. 고길동, 서울시 성동구, 029575976, Go@naver.com 을 기재한다.
    index = originalAddressBook.record("고길동", "서울시 성동구",
            "029575976", "Go@naver.com");
    System.out.printf("3. 주소록에 Record한 내용 GetAt : " ).println
            (originalAddressBook.getAt(index));
    //4. 홍길동, 인천시 연수구, 0313267926, Hong@gmail.com 을 기재한다.
    index = originalAddressBook.record("홍길동", "인천시 연수구",
            "0313267926", " Hong@gmail.com");
    System.out.printf("4. 주소록에 Record한 내용 GetAt : " ).println
            (originalAddressBook.getAt(index));
    //5. 최길동, 서울시 용산구, 023517134, Choi@를 기재한다.
    index = originalAddressBook.record("최길동", "서울시 용산구",
            "023517134", "Choi@naver.com");
    System.out.printf("5. 주소록에 Record한 내용 GetAt : " ).println
            (originalAddressBook.getAt(index));
    //6. 정길동, 서울시 종로구, 024366751, Jung@를 기재한다.
    index = originalAddressBook.record("정길동", "서울시 종로구",
            "024366751", "Jung@naver.com");
    System.out.printf("6. 주소록에 Record한 내용 GetAt : " ).println
            (originalAddressBook.getAt(index));
    //7. 나길동, 서울시 강서구, 0215749903, Na@naver.com 을 기재한다.
    index = originalAddressBook.record("나길동", "서울시 강서구",
            "0215749903", "Na@naver.com");
    System.out.printf("7. 주소록에 Record한 내용 GetAt : " ).println
            (originalAddressBook.getAt(index));
    //8. 원본 내용 출력하기
    System.out.printf("\n");
    System.out.printf("8. 원본 내용 출력하기 : \n");
    originalAddressBook.printAddressBook(8);
}

이를 메인메소드에서 출력해보면 아래 결과처럼 나옵니다. 원본초기내용

originalAddressBook 현재 상태 메모리맵

말로만 하면 설명이 힘들거 같아서 제가 이해하기 위해 직접 그린 메모리맵을 첨부합니다.

ArrayList의 경우 간단하게 메모리맵에 표현하기 위해 elementData(Object[] 배열), size(int 배열요소 개수)만 표시했습니다.

메소드 영역은 편의상 생략하였고, String역시 객체이기 때문에 기본자료형처럼 표시하면 안되지만 String객체의 특성을 반영하여(값을 변경하면 그 값은 그대로 남고 새로운 객체를 생성) 편의상 기본자료형처럼 표시하였습니다.

아까 위에서 AddressBook의 객체에 record된 Personal의 참조 주소값들은 이 ArrayList<Personal>의 elementData에 저장됩니다.

그리고 elementData의 각 배열요소인 Personal의 참조 주소값들은 각각의 Personal객체들을 가리키고 있습니다.

Personal 객체는 1개만 모든 필드를 다 그렸고 나머지는 편의상 name필드만 그리고 나머지 필드들은 생략하였습니다.

ArrayList_clone시작전 메모리맵

ArrayList의 clone을 통해 copyAddressBook 생성

여기서 아래의 코드처럼 ArrayList의 clone을 호출하는 얕은 복사를 하게 되면

public static void main(String[] args)
{
    //9. clone을 이용한 복사본 출력하기
    AddressBook copyAddressBook = originalAddressBook.clone();
}

ArrayList의 clone을 통해 copyAddressBook 메모리맵

ArrayList의 clone을 통해 얕은 복사를 하게 되면 메모리 맵의 상태는 아래와 같이 됩니다. ArrayList_clone후 메모리맵 위에서 제시한 ArrayList의 clone메소드 코드를 참고하여 메모리맵을 보시면 super.clone()를 통해 복사본(v)은 원본(originalAddressBook)과 별도의 공간(ArrayList<Personal>)을 힙영역에 생성합니다.

그리고 Arrays.copyOf(elementData, size)를 통해 별도의 elemetData 공간을 가지게 되고, 이 복사본(v)의 주소를 리턴받아 copyAddressBook에 저장합니다.

하지만 이 때, elementData의 각 배열요소들의 값은 그대로 복사됩니다.

각 배열요소들이 Personal객체의 주소값이기 때문에 이 주소값이 그대로 복사되면 결국에 원본과 복사본의 elementData는 참조주소값을 저장하는 별도의 배열 공간은 가지고 있지만 그 배열요소의 값은 같기 때문에 위의 메모리맵처럼 같은 Personal객체를 가리키게 됩니다.

이제 복사가 제대로 되었는지 확인을 하기 위해 복사본을 main메소드에서 출력해봅니다.

public static void main(String[] args)
{
    //9. 복사생성자를 이용한 복사본 출력하기
    //AddressBook copyAddressBook = new AddressBook(originalAddressBook);
    AddressBook copyAddressBook = originalAddressBook.clone();
    System.out.printf("\n");
    System.out.printf("9. 복사생성자를 이용한 복사본 출력하기 : \n");
    copyAddressBook.printAddressBook(9);
}

복사본초기내용

위에선 본 것 같이 복사가 제대로 된 것을 알 수 있습니다.

이제 각각의 Personal주소를 배열요소로 저장하고 있는 elementData를 건드려 보면서(?) 얕은 복사에서는 어떨 때 문제가 발생하는지 알아보도록 하겠습니다.

얕은 복사 후 복사본 정렬하기

이제 원본(originalAddressBook)은 그대로 두고 위에서 정의한 arrange를 통해 복사본(copyAddressBook)을 정렬한 뒤에 원본과 복사본에 저장된 Personal들을 전체 출력한 후에 그 출력 순서를 비교해봅시다.

public static void main(String[] args)
{
    //10. 이름을 기준으로 복사본을 오름차순 정렬하기
    System.out.printf("\n");
    System.out.println("10. 이름을 기준으로 복사본을 오름차순 정렬하기 : 복사본 정렬됨");
    copyAddressBook.arrange();
    copyAddressBook.printAddressBook(10);
    //11. 복사본 정렬한 후에 원본 출력하기
    System.out.printf("\n");
    System.out.println("11. 복사본 정렬한 후에 원본 출력하기 : 원본 정렬안됨");
    originalAddressBook.printAddressBook(11);
}

복사본정렬후 원본과비교
복사본은 가나다 순으로 정렬이 되어 있고, 원본은 정렬 안된 채로 그대로인것을 알 수 있습니다.

분명히 복사본에 손을 댔는데 원본이 그대로이기 때문에 복사가 잘되었다고 착각(?)을 할 수 있습니다.

이 상황을 설명하기 위해 복사본을 정렬한 후의 메모리맵을 첨부합니다.

얕은 복사 후 복사본 정렬하기 메모리맵

이름 오름차순 정렬후 원본 복사본 비교
빨간색 선을 유의해서 보시면 복사본의 elementData의 배열 인덱스 위치만 이름기준으로 오름차순으로 바뀐 것을 알 수 있습니다.

즉, 얕은 복사라도 정렬하기의 경우에는 원본의 내용(순서)이 유지됩니다.

나중에 복사본과 원본을 비교할 때 바뀐 사항을 쉽게 확인하기 위해 일단 원본도 정렬하기로 하고, 다음 체크사항으로 배열요소에서 지우기(erase)를 했을 때 얕은 복사의 경우 원본과 복사본이 어떻게 되는지 확인해보도록 하겠습니다.

얕은 복사 후 복사본 지우기

앞에서 정의한 find로 복사본에서 홍길동을 찾고, 찾은 홍길동 중에 첫번째 홍길동의 위치를 erase메소드에 입력하여 복사본에서 홍길동을 지워보도록 합시다.

public static void main(String[] args)
{
    //복사본에서 홍길동 찾기
    ArrayList<Integer> indexes = copyAddressBook.find("홍길동");
    //복사본에서 이름(홍길동)으로 찾은 내용 중 1번째 홍길동을 지우고,
    //지운 것을 확인하기 위해 전체 복사본 출력
    System.out.printf("14. 복사본에서 이름(홍길동)으로 찾은 내용 중 1번째 홍길동을 지우고" +
            "지운 것을 확인하기 위해 전체 복사본 출력\n");
    index = copyAddressBook.erase(indexes.get(0));
    if(index == -1)
    {
        System.out.printf(" 14.1 첫번째 홍길동이 지워졌습니다.\n");
        System.out.println(" 14.2 홍길동을 지운 후 복사본에 있는 내용 개수 : "
                + copyAddressBook.getPersonals().size());
        System.out.println(" 14.3 복사본에 저장된 전체 Personal 출력 : " );
        index = 0;
        while(index < copyAddressBook.getPersonals().size())
        {
            personal = copyAddressBook.getAt(index);
            System.out.printf("  14.3.%d ", index + 1).println(personal);
            index++;
        }
    }
    //그리고 복사본과 변경 사항을 비교하기 위해 원본도 전체 출력
    originalAddressBook.printAddressBook(15);
}

복사본에서 지운 후 원본과 복사본비교

결과를 보시면 복사본은 Personal객체 하나(홍길동)가 지워지면서 개수가 5개로 변경이 되었고, 원본은 그대로 개수가 6인것을 알 수 있습니다.

즉 복사본에서 삭제를 한 경우에도 원본에는 손상이 가지 않습니다.

이 상황을 메모리맵으로 나타내면 아래와 같습니다.

얕은 복사 후 복사본 지우기 메모리맵

복사본에서 이름지우기 위의 메모리맵에서 보시다시피 복사본의 elementData에서 해당Personal(홍길동)을 가리키고 있는 주소값만 삭제 되었습니다.

즉, 여전히 원본에서는 해당Personal(홍길동)을 가리키고 있기 때문에 홍길동Personal은 가비지컬렉터에 의해서 지워지지 않습니다.(GC의 경우 그 객체를 가리키고 있는 참조값이 하나도 없을 경우 메모리에서 할당해제합니다.)

이제 이를 통해서 우리는 복사본에 새로운 Personal객체를 추가(record)하더라도 원본은 그대로 유지되고 복사본만 변경될 것임을 추측할 수 있습니다.

(왜냐하면 record를 할 때 전달받은 매개변수로 새로운 Personal객체를 생성하여 복사본의 별도 저장공간인 elementData에 추가하는 것이기 때문에 원본과는 상관이 없음!)

즉, 얕은 복사를 하더라도 복사본에 추가, 삭제, 정렬하기의 경우에는 원본은 그대로 유지되는 이유를 메모리맵을 직접 그려보면서 알 수 있었습니다.

이제 얕은 복사의 경우에 문제가 되는 경우를 확인하기 위해 correct(수정하기)를 호출합니다.

얕은 복사 후 Personal 객체 내용 변경하기

이제 얕은 복사를 했을 떄 correct를 하면 어떻게 되는지 메모리맵을 통하여 확인해보도록 하겠습니다.

ArrayList<Integer> indexes = copyAddressBook.find("고길동");
int index = copyAddressBook.correct(indexes.get(0), "서울시 도봉구",
        "021119999", copyAddressBook.getAt(indexes.get(0)).getEmailAddress());

얕은 복사 후 Personal 객체 내용 변경하기 메모리맵

복사본에서 내용 변경하기 위의 메모리맵을 보시면 원본(originalAddressBook)과 복사본(copyAddressBook)의 element의 배열요소에서 동일한 Personal 객체인 “고길동”을 가리키고 있는 것을 알 수 있습니다.

그래서 복사본에서 “고길동”의 정보(주소, 전화번호)를 변경하면 원본의 ‘고길동’ 정보(주소, 전화번호) 역시 내용이 바뀌게 되는 것입니다.

깊은 복사를 할 수 있도록 clone 재정의

실생활에서는 책을 복사한 뒤에 그 복사본을 건드려도 원본은 항상 그대로입니다.

이와 마찬가지로 프로그래밍에서도 복사를 할거면 복사본에 손을 대도 원본은 그대로인 깊은 복사를 해야 한다고 생각합니다.

그래서 깊은 복사를 할 수 있도록 clone을 재정의하면 아래와 같습니다.

public class VisitingCardBinder implements Cloneable
{
    //깊은 복사
    @Override
    public AddressBook clone() throws CloneNotSupportedException
    {
        //새로운 주소록(복사본) 생성
        AddressBook addressBook = new AddressBook();
        //주소록(원본)의 처음부터 마지막까지 반복한다.
       for(Personal personal : this.personals)
        {
            //원본의 Personal객체를 깊은 복사하여 복사본에 추가한다.
            addressBook.personals.add(personal.clone());
        }
        //복사본을 반환한다.
        return addressBook;
    }
}

우선 복사본인 AddressBook객체를 생성합니다.

AddressBook의 디폴트 생성자를 호출하면 생성자 내부에서 ArrayList<Personal>을 생성하여 힙에 할당합니다.

그 이후 for each 반복을 통하여 원본의 ArrayList<Personal>this.personals의 각 배열요소의 Personal들을 구한 다음 clone 이용하여 이를 복사한 뒤에 복사본에 add합니다.

깊은 복사를 할 수 있도록 재정의한 후에 clone 메모리맵

깊은 복사생성자 메모리맵 위의 메모리맵처럼 복사본에 elementData들의 배열요소들이 원본과 내용은 똑같지만 별도의 공간인 Personal객체를 가리키고 있습니다.

깊은 복사 후 복사본의 Personal 객체 내용 변경하기

복사생성자의 코드만 깊은 복사를 할 수 있는 코드로 변경하고, 나머지는 코드는 그대로 하여 다시 실행해보면 결과는 아래처럼 복사본의 ‘고길동’의 주소와 전화번호가 변경되었지만 원본의 ‘고길동’의 주소와 전화번호는 그대로인 것을 알 수 있습니다.
(복사본은 원본에서 깊은 복사를 한 후 “홍길동”객체가 하나 지워지 상태임!) 깊은 복사 후 원본과 복사본 비교

깊은 복사 후 복사본의 Personal 객체 내용 변경하기 메모리맵

위의 결과를 메모리맵으로 나타내면 다음과 같습니다.
깊은 복사 후 내용변경하기 메모리맵

테스트코드

얕은 복사와 깊은 복사의 차이점을 비교하기 위해 main메소드에서 테스트를 하다보니 AddressBook의 모든 메소드를 이용하였고, 문제없이 잘돌아감을 확인할 수 있었습니다.

package AddressBook;

import java.util.ArrayList;

public class Main
{

    public static void main(String[] args) throws CloneNotSupportedException
    {
         System.out.println("AddressBook 메인테스트");
        //1. AddressBook 기본생성자 호출
        AddressBook originalAddressBook = new AddressBook();
        System.out.println("1. AddressBook 기본생성자 호출 : " 
        + originalAddressBook.getPersonals());
        //2. 주소록에 홍길동, 서울시 중구, 021766710, Hong@naver.com 을 기재한다.
        int index = originalAddressBook.record("홍길동", "서울시 중구",
                "021766710", "Hong@naver.com");
        System.out.printf("2. 주소록에 Record한 내용 GetAt : " ).println
                (originalAddressBook.getAt(index));
        //3. 고길동, 서울시 성동구, 029575976, Go@naver.com 을 기재한다.
        index = originalAddressBook.record("고길동", "서울시 성동구",
                "029575976", "Go@naver.com");
        System.out.printf("3. 주소록에 Record한 내용 GetAt : " ).println
                (originalAddressBook.getAt(index));
        //4. 홍길동, 인천시 연수구, 0313267926, Hong@gmail.com 을 기재한다.
        index = originalAddressBook.record("홍길동", "인천시 연수구",
                "0313267926", " Hong@gmail.com");
        System.out.printf("4. 주소록에 Record한 내용 GetAt : " ).println
                (originalAddressBook.getAt(index));
        //5. 최길동, 서울시 용산구, 023517134, Choi@를 기재한다.
        index = originalAddressBook.record("최길동", "서울시 용산구",
                "023517134", "Choi@naver.com");
        System.out.printf("5. 주소록에 Record한 내용 GetAt : " ).println
                (originalAddressBook.getAt(index));
        //6. 정길동, 서울시 종로구, 024366751, Jung@를 기재한다.
        index = originalAddressBook.record("정길동", "서울시 종로구",
                "024366751", "Jung@naver.com");
        System.out.printf("6. 주소록에 Record한 내용 GetAt : " ).println
                (originalAddressBook.getAt(index));
        //7. 나길동, 서울시 강서구, 0215749903, Na@naver.com 을 기재한다.
        index = originalAddressBook.record("나길동", "서울시 강서구",
                "0215749903", "Na@naver.com");
        System.out.printf("7. 주소록에 Record한 내용 GetAt : " ).println
                (originalAddressBook.getAt(index));
        //8. 원본 내용 출력하기
        System.out.printf("\n");
        System.out.printf("8. 원본 내용 출력하기 : \n");
        originalAddressBook.printAddressBook(8);
        //9. 복사생성자를 이용한 복사본 출력하기
        //AddressBook copyAddressBook = new AddressBook(originalAddressBook);
        AddressBook copyAddressBook = originalAddressBook.clone();
        System.out.printf("\n");
        System.out.printf("9. 복사생성자를 이용한 복사본 출력하기 : \n");
        copyAddressBook.printAddressBook(9);
        //10. 이름을 기준으로 복사본을 오름차순 정렬하기
        System.out.printf("\n");
        System.out.println("10. 이름을 기준으로 복사본을 오름차순 정렬하기 : 복사본 정렬됨");
        copyAddressBook.arrange();
        copyAddressBook.printAddressBook(10);
        //11. 복사본 정렬한 후에 원본 출력하기
        System.out.printf("\n");
        System.out.println("11. 복사본 정렬한 후에 원본 출력하기 : 원본 정렬안됨");
        originalAddressBook.printAddressBook(11);
        //12. 나중에 복사본과 원본을 비교할 때 바뀐 사항을 쉽게 확인하기 위해 원본도 정렬하기
        System.out.printf("\n");
        System.out.println("12. 나중에 복사본과 원본을 비교할 때 
        바뀐 사항을 쉽게 확인하기 위해 원본도 정렬함 : ");
        originalAddressBook.arrange();
        originalAddressBook.printAddressBook(12);
        //13. 복사본의 내용을 변경하기 위해 복사본에서 변경할 배열요소를 이름(홍길동)으로 찾기
        System.out.printf("\n");
        System.out.println("13. 복사본의 내용을 변경하기 위해 복사본에서" +
                " 변경할 배열요소를 이름(홍길동)으로 찾기 : ");
        ArrayList<Integer> indexes = copyAddressBook.find("홍길동");
        index = 0;
        Personal personal = null;
        if(indexes.isEmpty() == false)
        {
            while(index < indexes.size())
            {
                personal = copyAddressBook.getAt(indexes.get(index));
                System.out.printf(" 13.%d ", index+1).println(personal);
                index++;
            }
        }
        //14. 복사본에서 이름(홍길동)으로 찾은 내용 중 1번째 홍길동을 지우고,
        //지운 것을 확인하기 위해 전체 복사본 출력
        System.out.printf("\n");
        System.out.printf("14. 복사본에서 이름(홍길동)으로 찾은 내용 중 1번째 홍길동을 지우고" +
                "지운 것을 확인하기 위해 전체 복사본 출력\n");
        index = copyAddressBook.erase(indexes.get(0));
        if(index == -1)
        {
            System.out.printf(" 14.1 첫번째 홍길동이 지워졌습니다.\n");
            System.out.println(" 14.2 홍길동을 지운 후 복사본에 있는 내용 개수 : "
                    + copyAddressBook.getPersonals().size());
            System.out.println(" 14.3 복사본에 저장된 전체 Personal 출력 : " );
            index = 0;
            while(index < copyAddressBook.getPersonals().size())
            {
                personal = copyAddressBook.getAt(index);
                System.out.printf("  14.3.%d ", index + 1).println(personal);
                index++;
            }
        }
        //15. 복사본 내용을 지운 후 원본 내용도 지워졌는지 확인하기 위해 원본 내용도 출력하기
        System.out.printf("\n");
        System.out.println("15. 복사본 내용을 지운 후 원본 내용도 지워졌는지
         확인하기 위해 원본 내용도 출력하기 : ");
        System.out.println(" 15.1 복사본에서 홍길동을 지운 후 원본에 있는 내용 개수 : "
                + originalAddressBook.getPersonals().size());
        index = 0;
        while(index < originalAddressBook.getPersonals().size())
        {
            personal = originalAddressBook.getAt(index);
            System.out.printf(" 15.2.%d ", index + 1).println(personal);
            index++;
        }
        //16. 복사본에서 고길동을 찾아서 고길동의 주소와 전화번호 변경하고,
        //변경사항 확인하기 위해 전체 복사본 출력
        System.out.printf("\n");
        System.out.printf("16. 복사본에서 고길동을 찾아서 고길동의 주소와 전화번호 변경하고 " +
                "변경사항 확인하기 위해 전체 복사본 출력\n");
        indexes = copyAddressBook.find("고길동");
        index = copyAddressBook.correct(indexes.get(0), "서울시 도봉구",
                "021119999", copyAddressBook.getAt(indexes.get(0)).getEmailAddress());
        copyAddressBook.printAddressBook(16);
        //17. 복사본 내용 변경 후 원본 내용도 변경됬는지 확인하기 위해 원본 내용도 출력하기
        System.out.printf("\n");
        System.out.println("17. 복사본 내용 변경 후 원본 내용도
         변경됬는지 확인하기 위해 원본 내용도 출력하기 : ");
        originalAddressBook.printAddressBook(17);
    }
}

마치며

이번 시간을 통해 주소록 프로그램의 핵심기능인 기재하기, 찾기, 수정하기, 지우기, 정렬하기 등을 구현하며서 이 메소드들을 통해 **최종적으로 ArrayList의 얕은 복사와 깊은 복사**에 대해 배울 수 있었습니다.

혹시나 틀린 내용이나 미흡한 내용이 있을 수 있습니다.

잘못된 또는 궁굼한 사항은 언제든지 댓글 또는 이메일을 남겨주시면 답변을 드리도록 하겠습니다.

다음 글은 AddressBook클래스의 find메소드를 구현하면서 깨달은 C++와 Java의 차이점에 대한 고찰 - 참조변수와 포인터, 메소드 반환(리턴) 개수에 대해 작성하도록 하겠습니다.

긴 글 읽어주셔서 감사합니다.

댓글남기기