Java/Java

복사 생성자와 싱글톤

마손리 2023. 3. 5. 18:27

이번 포스팅은 자바를 이용하여 간단한 Kiosk 프로토타입을 만들어 보던중 발생한 몇가지 버그에 대한 글이다.

(Git : https://github.com/Mason3144/burger_shop-kiosk-practice )

 

복사 생성자(Copy Constructor)

장바구니 기능을 구현하던중 발생한 버그이다. 처음 의도한 결과로는

  1. 메뉴에서 하나의 제품을 선택한뒤 옵션을 선택해주고 장바구니에 넣는다.
  2. 다시 동일한 제품을 선택하게되면 모든 옵션들이 초기화되어 다시 옵션을 선택해준다. 

이었다. 

 

하지만 처음 의도한 것과는 달리 동일한 제품을 선택하게 되면 옵션들이 전에 선택했던 옵션 그대로 저장되어 나타나게 되었다.

 

문제의 발단은 메뉴의 모든 제품(음식 객체)들의 정보를 담은 배열에서 하나의 객체를 선택, 이후 선택된 원본 객체를 배열에서 불러와 옵션값들을 설정하는것이 문제였다.  

 

얕은 복사(Shallow copy)

이러한 현상이 일어나는 원인은 선택된 특정한 객체의 참조값만을 복사하여 새로운 변수에 저장하게 되어 배열안의 원본 객체와 변수에 저장된 객체가 같은 참조값을 가지게되어 결국 하나의 동일한 객체가 되기 때문이며 이를 얕은 복사라 한다.

 

예제)

public class ShallowCopyExample {
    void operator(){
        Product[] cart = new Product[1]; //장바구니 인스턴스

        Product[] menu = new Product[]{ //제품 인스턴스들이 보관되어있는 메뉴 인스턴스
                new Product1("first"),
                new Product2("second")
        };

        Product newProduct = menu[0]; // 메뉴에서 특정 제품의 원본 인스턴스를 불러와 지역변수에 할당
        newProduct.isOption=false; // 지역변수에 할당된 원본 인스턴스의 옵션을 변경

        cart[0] = newProduct; // 선택된제품 장바구니에 담기

        //결과확인
        System.out.println(menu[0].isOption); //false
        System.out.println(cart[0].isOption); //false

        newProduct.isOption=true;

        System.out.println(menu[0].isOption); //true
        System.out.println(cart[0].isOption); //true
    }
}

원본 인스턴스의 참조값newProduct라는 지역변수에 전달해주고 다시 같은 지역변수를 가진 newProduct를 장바구니에 넣어 주었으므로 menu[0], newProduct, cart[0]은 모두 같은 객체이다. 

 

이를 해결하기 위해서는 newProduct새로운 객체를 생성한뒤 원본객체의 정보만을 넘겨주는방식을 사용해야하며 이를 깊은복사라한다.

 

깊은복사(Deep copy)

깊은복사는 객체가 가지고 있는 정보들은 동일하지만 참조값이 다른 새로운 객체를 생성하는것을 의미한다.

 

예제)

public class ShallowCopyExample {
    void operator(){
        Product[] cart = new Product[1]; //장바구니 인스턴스

        Product[] menu = new Product[]{ //제품 인스턴스들이 보관되어있는 메뉴 인스턴스
                new Product1("first"),
                new Product2("second")
        };

        Product newProduct = new Product1(menu[0].name);
        //메뉴에서 원본 객체의 정보만을 불러와 새 객체에 전달, 이를 복사 생성자라 함
        
        newProduct.isOption=false; // 지역변수에 할당된 새 객체의 옵션을 변경

        cart[0] = newProduct; // 새 객체를 장바구니에 담기

        //결과확인
        System.out.println(menu[0].isOption); //false
        System.out.println(cart[0].isOption); //false

        newProduct.isOption=true;

        System.out.println(menu[0].isOption); //false
        System.out.println(cart[0].isOption); //true
    }
}

지역변수 newProduct에 원본 객체(객체의 참조값)을 할당해주는 것이 아닌 새로운 객체를 만들어 원본객체가 가진 정보만을 전달해줌으로써 menu[0]와 cart[0], 두객체는 서로 독립된 완전히 다른 두 객체가 된다.

 

이처럼 자신과 같은 클래스의 인스턴스를 인수로 받아 깊은 복사를 수행해주는 생성자를 복사 생성자라 한다.

 

 

싱글톤(Singleton pattern)

첫번째 문제의 경우 서로 독립된 두개의 객체들이 필요한 경우였던 것과 달리 두번째 문제는 서로 독립된 객체들 때문에 문제가 발생했다.

 

예제)

public class Main {
    public static void main(String[] args) {
        Cart cart = new Cart();
        Order order = new Order();

        cart.addToCart(new Product("item")); // "item"이라는 제품을 장바구니에 담기
        order.order();
        //주문클래스의 order()메서드를 이용하여 장바구니 클래스의 
        //checkIntoCart()메서드를 호출하여 장바구니에 담긴 제품들 확인
    }

	//장바구니 클래스
    static class Cart { 
        Product[] cart = new Product[0];

        void addToCart(Product product){
            cart = Arrays.copyOf(cart, cart.length+1);
            cart[cart.length-1]= product;
        }//매개변수로 전달받은 제품을 장바구니에 담기
        
        void checkIntoCart(){
            for(Product product:cart) System.out.println(product.name);
        };// 장바구니에 담긴 제품들 출력
    }
    
	//주문 클래스
    static class Order {
        Cart cart = new Cart();
        void order(){
            cart.checkIntoCart();
        }//주문을 진행하기전에 장바구니 클래스의 매서드를 호출하여 장바구니에 들어있는 제품 확인
    }
}

위와 같이 두개의 내부클래스(예제를 위해 간단하게 내부클래스 이용) , 장바구니 클래스와 주문 클래스를 작성해주었다. 이후 장바구니에 물품을 한개 추가해주고 주문 클래스의 메서드를 통해 장바구니 클래스의 메서드를 호출해 주었지만 추가된 물품이 출력되지 않았다.

 

문제의 원인은 Main 클래스에 생성된 Cart 인스턴스와 Order 클래스에 생성된 Cart는 두개의 독립된 다른 인스턴스이기 때문이다. 그렇기에 addToCart()메서드를 통해 생성된 Product 인스턴스는 Main 클래스에 생성된 장바구니에 존재하며 Order 클래스에 생성된 장바구니는 비어있는 것이다. 

 

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        Cart cart = new Cart();
        
        Order order = new Order(cart);//생성된 Cart인스턴스를 Order의 생성자에 매개변수로 전달
	//Order order = new Order();-이전코드
    
        cart.addToCart(new Product("item"));
        order.order();
    }
    
    // Cart class는 생략 //
    
    static class Order {
	//Cart cart = new Cart();-이전코드   
        Cart cart;
        public Order(Cart cart) {
            this.cart = cart;
        }
        // Cart타입의 지역변수(혹은 전역변수)에 선언해준뒤 생성자를 통해 인스턴스를 전달받음
        
        void order(){
            cart.checkIntoCart();
        }
    }
}
// 출력
item

Cart 인스턴스를 두번 생성하여 두개의 독립된 인스턴스들을 가진 이전의 코드와 달리 이번코드는 main()메서드에서 Cart인스턴스를 한번만 생성해주고 생성된 인스턴스를 다른 클래스의 생성자를 통해 매개변수로 전달, 이후 전달받은 클래스에서 지역변수(혹은전역변수)에 전달받은 인스턴스를 할당 하여 사용해주는 방식을 사용하였으며 이와같이 단 하나의 객체만 생성되도록 코드를 작성하는 패턴을 싱글톤 패턴(Singleton pattern)이라고 한다.