블로그 이미지

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 불변 객체는 어떻게 만드는가?
    Programming/Java 2022. 3. 4. 03:23

    불변 객체란?

    불변 객체는 생성된 시점 이후로 상태가 변하지 않는 객체를 뜻한다. 예를 들어, 다음과 같이 Lottery 클래스가 있다고 하자.

    public class Lottery {
    	private Set<LotteryNumber> numbers;
        
    	public Lottery(final Set<LotteryNumber> numbers) {
        	this.numbers = numbers;
        }
    }

    위와 같은 Lottery 클래스를 클라이언트 코드에서 객체 생성한다고 하면, Lottery의 인스턴스 필드인 numbers의 상태가 주입되게 된다. 그런데, 불변 객체는 이렇게 생성된 시점에서 주입된 상태가 변하지 않음을 보장하고 유지되는 것을 말한다.

    왜 불변 객체를 만들어야 하는가?

    불변 객체를 보장함으로써 얻는 이점은 다양하다. 

    1. 불변 객체는 Thread-Safety를 보장하여 동시성 이슈를 걱정하지 않아도 된다.
    2. 불변이 보장되기 때문에 Map의 key와 Set의 원소로 쓰기 좋다.
    3. 불변 객체임이 보장된다면 어떠한 메세지를 보내더라도 해당 객체의 상태가 변하지 않는다는 확신을 얻을 수 있기 때문에 협업 시 다른 사람이 작성한 기능이더라도, 사용하며 심신의 안정(?)을 얻을 수 있다.
    4. 실패 원자성을 보장할 수 있다.

    등의 장점이 있는데, 이 중에서 Thread-Safety와 실패 원자성 보장이라는 장점의 필요성을 아래의 Car 클래스를 통해 간략히 설명해보고자 한다.

     

    public class Car {
    	private int position;
        
        public Car(){
        	this.position = 0;
        }
        
        public int move(){
        	this.position += 1;
            if (this.position > 5) {
            	throw new IllegarArgumentException();
            }
           	return this.position;
        }
    }

     

    먼저, 실패 원자성에 대해 설명하자면, Car의 move라는 기능(정상적인 로직이 아니지만 예시니까 감안을...)을 해당 객체의 position이 5인 상태에서 이용하면 position 값은 6이 되고, 에러를 발생시킨다. 원래는 클라이언트 코드에서 position 값을 반환받아야 했지만 에러가 발생했기 때문에 반환받지 못하였다. 그럼 클라이언트 코드를 작성하는 입장에서는 값을 반환받지 못하였기 때문에 position값이 그대로일 것이라고 생각할 것이다. 하지만, position은 이미 1이 올라간 뒤 에러를 발생시킨 것이기 때문에 position값은 변화하였다. 기능을 이용하면서 에러가 발생했는데, 완전히 실패한 것이 아니라 반만 실패한 것이 되는 것이다.

     

    다음으로 Thread-Safety에 대해 말해보자면, 먼저 Car 객체가 position값이 4인 상황이라고 가정하자. A 쓰레드에서 move 기능을 사용하였고, position 값이 1 올라간 뒤에 if 조건식에서 5를 초과하지 않으니 position값이 5인 상태로 반환하려는 순간, B 쓰레드에서 해당 객체의 move 기능을 이용한다. A,B 쓰레드는 주소를 공유하고 있기 때문에 B 쓰레드에서 position 값이 1 올라가는 순간, A 쓰레드에서 반환하려던 5의 position값이 6으로 변하게 되며 클라이언트는 예상치도 못한 6이라는 값을 얻게 된다.

     

    위와 같은 문제들을 불변 보장을 통해 해결할 수 있다!

    그럼, 불변 객체는 어떻게 만들까?

    불변 객체를 만드는 방법은 다음과 같다.

    1. 클래스를 확장하지 못하도록 막는다.
    2. 모든 필드를 final로 선언하여 생성 이후로 값을 변경하지 못하게 막는다.
    3. setter를 제공하지 않는다.
    4. 모든 필드를 private으로 선언하여 직접적인 접근을 막는다.
    5. getter에서 반환되는 필드가 불변 혹은 read-only 여야 한다.
    6. 매개변수를 받는 생성자인 경우 deep copy를 실시한다.

    맨 처음 예로 들었던 Lottery 클래스를 불변 객체로 만들어 보며 실습해보도록 하겠다.

    먼저, 클래스를 확장하지 못하도록 막는다. 이는 하위 클래스에서 객체의 상태를 변하게 만들 수도 있기 때문에 그런 상황을 방지하기 위함이다.

    public final class Lottery {
    }

    위와 같이 클래스 앞에 final을 붙여 확장하지 못하도록 막을 수도 있지만, 정적 팩터리 메서드를 이용하는 방법도 있다. 생성자를 private으로 제한하고, 정적 팩터리 메서드를 통해 객체를 생성하는 것이다. 생성자를 private으로 제한하기 때문에 다른 패키지에서 이 클래스를 확장하는 것이 불가능하다.

    public class Lottery {
    	private Set<LotteryNumber> numbers;
        
    	private Lottery(final Set<LotteryNumber> numbers) {
        	this.numbers = numbers;
        }
        
        public static Lottery from(final Set<LotteryNumber> numbers) {
        	return new Lottery(numbers);
        }
    }

    위와 같이 정적 팩터리 메서드를 이용하여 확장을 제한할 수 있다.

    다음은, 모든 필드를 private final로 선언하는 것이다. 이와 함께 getter까지 같이 작성해보도록 하겠다.

    public class Lottery {
    	private final Set<LotteryNumber> numbers;
        
    	private Lottery(final Set<LotteryNumber> numbers) {
        	this.numbers = numbers;
        }
        
        public static Lottery from(final Set<LotteryNumber> numbers) {
        	return new Lottery(numbers);
        }
        
        public Set<LotteryNumber> getNumbers() {
    		return Collections.unmodifiableSet(numbers);
    	}
    }

    위 코드를 보면, getter 메서드에서 unmodifiableSet을 통해 반환하는 것을 볼 수 있다. 그런데, 이를 통해 반환하면 정말로 numbers 필드가 불변임을 보장할 수 있을까?

    	@Test
    	void ttt() {
    		//given
    		final Set<LotteryNumber> numbers = new TreeSet<>();
    		numbers.add(new LotteryNumber(1));
    		Lottery lottery = Lottery.from(numbers);
    		//when
    		numbers.add(new LotteryNumber(2));
    		Set<LotteryNumber> numbers2 = lottery.getNumbers();
    		//then
    		assertThat(numbers2.size()).isEqualTo(1);
    	}

    위의 테스트는 과연 성공할까? 예상했겠지만 실패다. 그 이유는 lottery를 생성할 때 넘겨준 numbers의 참조값이 lottery의 필드인 numbers가 참조하고 있는 값과 같기 때문이다. 그렇기 때문에 마지막 6번, 생성할 때 deep copy를 실시해주어야 하는 이유이다.

    public class Lottery {
    	private final Set<LotteryNumber> numbers;
        
    	private Lottery(final Set<LotteryNumber> numbers) {
        	this.numbers = new TreeSet<>(numbers);
        }
        
        public static Lottery from(final Set<LotteryNumber> numbers) {
        	return new Lottery(numbers);
        }
        
        public Set<LotteryNumber> getNumbers() {
    		return Collections.unmodifiableSet(numbers);
    	}
    }

    위의 코드에서 생성자 부분을 보면 새로운 TreeSet을 생성해 필드에 새로운 참조값을 할당해주는 것을 볼 수 있다. 이를 통해 Set이나 List 등의 자료구조에서도 불변임을 보장할 수 있게 된다!

     

    참조

    Effective Java 3/E

    https://www.yegor256.com/2014/06/09/objects-should-be-immutable.html

     

    Objects Should Be Immutable

    The article gives arguments about why classes/objects in object-oriented programming have to be immutable, i.e. never modify their encapsulated state

    www.yegor256.com

    https://www.geeksforgeeks.org/create-immutable-class-java/

     

    How to create Immutable class in Java? - GeeksforGeeks

    A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

    www.geeksforgeeks.org

    https://www.linkedin.com/pulse/20140528113353-16837833-6-benefits-of-programming-with-immutable-objects-in-java/

     

    6 Benefits of Programming with Immutable Objects in Java

    Immutability is often presented as a key concept of functional programming. Most functional programming languages like Haskell, OCaml and Scala follow a immutable-by-default approach for variables in their programs.

    www.linkedin.com

     

    'Programming > Java' 카테고리의 다른 글

    String.matches() VS Pattern.compile()  (0) 2022.02.18

    댓글

Designed by Tistory.