생성자의 다양성 => aip 사용자, 클라이언트에게 긍정적인 사용성 제공
but
메소드의 다양성 => 객체의 복잡도와 중복 코드를 증가시킴 => 단일 책임의 원칙 위배 가능성 증가
그렇다고 생성자가 다 할려고 하지 말자
생성자는 필드에 데이터를 받는 역할만 해야 한다.
생성자가 할 일이 많다면 팩터리 메소드를 활용하자.
: 직접적으로 생성자를 통해 객체를 생성하는 것이 아닌 메서드를 통해서 객체를 생성하는 것을 정적 팩토리 메서드라고 한다.
"생성자 대신 정적 팩토리 메서드를 고려하라" -이펙티브 자바-
굳이 생성자가 있는데 팩토리 메소드를 사용하는 이유는 뭘까?
테스트가 쉬운 부분과 어려운 부분을 분리하자.
메소드 내부에서 결정되는 상수 값을 인위적으로 외부에서 결정할 수 있도록 한다.
테스트 진행
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(변수 혹은 필드 값이 변경되거나 설정되는 등의 변화)에 대한 걱정에서 자유로울 수 있다.
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에서 헛점이 보이는 군요"
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 메모리를 할당해서 반환시켜준다.
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