테스트만을 위한 메소드의 필요성, 가변 불변 객체

Jay_u·2023년 1월 22일
0

Java

목록 보기
4/8

TDD 방법론을 지향한다고 해서 테스트만을 위한 생성자와 메소드를 구현해야 하나?

생성자의 다양성 => aip 사용자, 클라이언트에게 긍정적인 사용성 제공

but

메소드의 다양성 => 객체의 복잡도와 중복 코드를 증가시킴 => 단일 책임의 원칙 위배 가능성 증가

그렇다고 생성자가 다 할려고 하지 말자
생성자는 필드에 데이터를 받는 역할만 해야 한다.
생성자가 할 일이 많다면 팩터리 메소드를 활용하자.


잠깐 팩토리 메소드란..?

: 직접적으로 생성자를 통해 객체를 생성하는 것이 아닌 메서드를 통해서 객체를 생성하는 것을 정적 팩토리 메서드라고 한다.

"생성자 대신 정적 팩토리 메서드를 고려하라" -이펙티브 자바-

굳이 생성자가 있는데 팩토리 메소드를 사용하는 이유는 뭘까?

  • 이름을 가지고 있어 메소드 이름에 객체의 생성 목적을 담을 수 있다.
  • 메소드이기에 하위 자료형 객체를 반납할 수 있다.
  • 객체 생성을 캡슐화할 수 있다.

상수 값을 포함한 객체 테스트 코드 작성을 용이하게 하는 방법

  1. 테스트가 쉬운 부분과 어려운 부분을 분리하자.

  2. 메소드 내부에서 결정되는 상수 값을 인위적으로 외부에서 결정할 수 있도록 한다.

  3. 테스트 진행


    ex)

    public Car {
       private static final int FORWARD_NUM = 4;
       private int position;
       
       ...
       
       public void move() {
       	if (getRandomNo() >= FORWARD_NUM) {
           	this.position++;
           }
       }
       
       public int getRandomNo() {
       	Random random = new Random();
           return random.nextInt(10);
       }
    }
    

Car 객체 내부의 getRandomNo는 랜덤으로 1~9 숫자를 반환한다고 할 때
move() 메소드의 테스트를 위해서 개발자는 getRandomNo의 반환값에 의존할 수 밖에 없다.

따라서 무브 메소드를 아래와 같이 바꿔서 테스트를 진행하면 개발자가 원하는 값을 넣어가며 테스트를 할 수 있다.

move(int num) {
    	if (num() >= FORWARD_NUM) {
       	this.position++;
       }
}

가변 객체와 불변 객체

가변 객체는 자바에서 class의 인스턴스 생성 이후 내부 상태가 변경 가능한 객체이다.

불변 객체는 가변 객체와 다르게 자바에서 class의 인스턴스 생성 이후 내부 상태를 변경할 수 없는 객체이다.

불변 객체는 read-only 메소드만 제공되며 객체의 내부 상태를 알려주는 메소드를 제공하지 않거나 제공할 경우 방어적 복사를 통해 제공한다.


이러한 불변 객체를 사용함으로써 여러 장점을 얻을 수 있다.

1. 객체 생성이후 해당 객체의 상태를 변경할 수 없기 때문에 설계 구현 및 사용하는데 편리하다.
2. side effect(변수 혹은 필드 값이 변경되거나 설정되는 등의 변화)에 대한 걱정에서 자유로울 수 있다.

  • 객체의 setter가 구현되어 있어 여러 메소드에서 객체의 값을 변경할 수 있다면 안정성이 떨어진다. 따라서 값의 수정이 불가능한 불변 객체를 통해 객체의 생성과 사용을 제한시켜 순수 함수를 만든다면 객체를 안전하게 재사용할 수 있게 된다.

불변객체라고 생각되는 오류

final 상수라고 지정하고 클래스에 setter을 사용하지 않으면 불변 객체일까??

> 그렇지 않다!


public class Main {

	public static void main(String[] args) {
    
    List<String> students = new ArrayList<Arrays.asList(("토비", "에이미", "제이슨"));
    
	Class class = new Class("1반", students);
    
	System.out.println(class);

	students.add("김미상");
	System.out.println(class);
    
	}
}



처음에 학생들의 목록(students)에는 토비, 에이미, 제이슨이 포함되어 있었다. 그리고 이를 반이라는 class라는 인스턴스를 생성할 때 주입하였다. 이후 주입한 students를 김미상씨를 추가로 넣어 조작했을 때 출력은 다음과 같다.


Class{name='1반', students=['토비', '에이미', '제이슨']}
Class{name='1반', students=['토비', '에이미', '제이슨', '김미상']}




"students의 주소가 공유되기 때문에 발생한 문제를 활용해서 해킹 완료 ㅎ"




그렇다 class라는 객체 생성시에 넘겨준 students는 주소가 공유되고 있었다. 이를 막기 뮈해서는 방어적 복사가 필요하다.



방어적 복사

방어적 복사란 생성자에서 new ArrayList<>()를 활용해서 메모리를 새로 할당하여 참조 주소를 끊어 내는 복사를 말한다.

이렇게 하면 인자로 받은 students 리스트와는 별개의 리스트를 생성해서 students를 복사할 수 있다.


	public Class(Stirng naem, List<String> students) {
    
      this.name = name;
      this.students = new ArrayList<>(students);

      // 대부분은 지금까지 this.students = students; 라고 작성했을 것이다.
    }




"그래도 아직 방법이 있습니다.. getter에서 헛점이 보이는 군요"




getter에도 방어적 복사를 활용하는 방법



	public Class(Stirng naem, List<String> students) {
    
      this.name = name;
      this.students = new ArrayList<>(students);
    }

    public List<String> getStudentsList() {  // students를 가져오는 getter이 있다면?
      return students;
    }
       



Classs의 getter메소드를 활용해서 가져온 students는 주소공유가 된다...



List<String> studentsOfClass = class.getStudentsList();
studentsOfClass.add("김미상");



이렇게 하면 김미상씨가 생성자 방어적 복사를 통해 막았던 주소를 뚫고 다시금 1반의 학생 목록에 들어갈 수 있게 된다.


	public Class(Stirng naem, List<String> students) {
    
      this.name = name;
      this.students = new ArrayList<>(students);
    }

    public List<String> getStudentsList() { 
      return new ArrayList<>(students);    // 리턴 값으로 새로운 list를 만들어서 getter 방어적 복사
    }
	



따라서 getter의 반환값에도 새로운 list 메모리를 할당해서 반환시켜준다.


/ reference

https://hyeon9mak.github.io/Java-test-and-immutable-and-object/
https://steady-coding.tistory.com/559
https://github.com/her0807/java-vendingmachine-precourse/wiki/%EC%9D%BC%EA%B8%89-%EA%B0%9D%EC%B2%B4%EC%99%80-%EC%9D%BC%EA%B8%89-%EC%BB%AC%EB%A0%89%EC%85%98

profile
정확한 정보를 전달할려고 노력합니다.

0개의 댓글