1주 차 자동차 미션을 진행하면서 주 생성자/부 생성자 패턴에 대한 피드백을 받았었습니다.
생성자에도 패턴이 있다는 사실 알고 계셨나요?🤔
수정 전) 생성자 각각 초기화하는 상황
두 개의 오버로딩된 생성자가 보이네요.
`name`만 인자로 받는 생성자와 `name`과 `movedDistance`를 함께 받는 생성자가 있습니다.
public class Car {
private final String name;
private final int movedDistance;
public Car(String name) {
this.name = name;
movedDistance = 0;
}
public Car(String name, int movedDistance) {
this.name = name;
this.movedDistance = movedDistance;
}
...
}
위 코드에 어떤 문제가 있을까요?
- 코드 중복 발생
- `this.name = name` 코드가 중복되고 있네요.
- 유지 보수에 불리
- 코드 중복과 비슷한 맥락입니다. 중복 코드가 많아질수록, 코드가 바뀔 때마다 수정해야 하는 코드가 많아지는 불편함이 있어요.
- 또한 Car 생성자에 validate 로직이 포함된다면, 모든 생성자에서 validate 메서드를 추가해줘야 합니다.
코드를 수정해 볼까요?
수정 후) 부 생성자에서 주 생성자 호출
public class Car {
private final String name;
private final int movedDistance;
public Car(String name) {
this(name, 0);
}
private Car(String name, int movedDistance) {
this.name = name;
this.movedDistance = movedDistance;
}
...
}
오 좋네요!
`this` 키워드로 부 생성자에서 주 생성자를 호출하고 있어요. 이를 생성자 체인(Constructor Chaining)이라고 합니다.
참고로, `this`를 사용해 다른 생성자를 호출할 때는 맨 첫 줄에서만 사용할 수 있습니다.
이제 모든 생성 로직은 주 생성자를 거쳐서 초기화가 되겠네요! 중복 코드가 사라지고, 유지보수성이 높아졌어요.
일반적으로, 주 생성자는 부 생성자의 맨 마지막에 위치합니다.
정적 팩토리 메서드
주 생성자 부생성자 패턴은 이제 알겠는데, 여러 생성자를 만드는 상황에서 꼭 이 방식을 택해야 할까요?
좀 더 근본적인 질문을 던져봅시다.
어떤 방식으로 객체를 생성하시나요?
생성자로 구현할까? vs 정적 팩토리 메서드로 구현할까?
"생성자 대신 정적 팩터리 메서드를 고려하라" 이펙티브 자바에서 고려하라는데, 정적 팩토리 메서드로 다 구현하는 게 좋은 거 아닌가?
여기서 잠깐! 정적 팩토리 메서드가 무엇일가요?
정적 팩토리 메서드란?
static 메서드로 간접적으로 생성자를 호출하여 객체를 생성하는 방식을 의미합니다.
위 코드를 아래처럼 바꿀 수 있습니다.
public class Car {
private final String name;
private final int movedDistance;
private Car(String name, int movedDistance) {
this.name = name;
this.movedDistance = movedDistance;
}
public static Car from(String name) {
return new Car(name, 0);
}
public static Car of(String name, int movedDistance) {
return new Car(name, movedDistance);
}
...
}
생성자를 `private`으로 닫아두고, `from`과 `of` 정적 메서드에서 생성자를 호출하고 있어요.
이 방식을 사용하면 외부에서는 마음대로 생성을 쉽게 하지 못하고 정적 팩토리 메서드를 통해서만 가능하겠네요!
정적 팩토리 메서드 컨벤션
말이 나온 김에, 컨벤션을 짚고 가겠습니다. 특히 생성 메서드에서 create이 더 직관적이라고 생각해 사용을 했었는데요, 잘못된 사용이었음을 알았습니다..
- `from` : 인자 1개를 받아 객체 생성, 반환
- `of` : 인자 여러 개를 받아 객체 생성, 반환
- `valueOf` : from, of의 구체적인 버전
- `instance` / `getInstance` : 인자가 있다면 명시된 객체 반환. 같은 객체인지는 보장하지 않음. 싱글톤 패턴에서 getInstance로 인스턴스를 가져오는 상황을 생각해 보자.
- `create` / `newInstance` : instance / getInstance와 유사하나, 항상 새로운 객체를 생성하여 반환
다시 본론으로 돌아와서,
정적 팩토리 메서드는 생성자를 간접적으로 호출해 준다는 건데, 굳이 써야 할까요?
1. 생성 이외에 다른 비즈니스 로직이 있을 때
예측 가능성 측면에서, '어? 뭔가 좀 더 특별한 로직이 있나 보네?'라는 의도를 전달할 수 있습니다.
미션을 진행하면서 받았던 피드백 중 일부입니다.
저는 객체 생성을 위한 검증 로직은 생성자에 두기도 하는데요, 웬만하면 생성자에는 그 외 로직을 넣지 않으려고 해요.
그 이유는 프로젝트 전체적으로 생성자를 사용할 때, 거기에 내가 넘겨 지나치는 로직이 있을까 봐 그래요. 적어도 정적 팩토리 메서드가 있다면 사용할 때 생성과 검증 외의 로직이 있을 수 있다는 걸 나타내기도 하고요.
Car 내부 로직을 모르는 팀원이 해당 클래스를 사용할 때, 다음과 같이 생각할 수 있겠죠?
생성자로 생성이 된다면 → 순수하게 생성해주고 있군!
정적 팩토리 메서드가 있다면 → 인자를 받아서 어떤 비즈니스 로직을 처리하고 생성해 주겠군!
(물론 위 Car 예시에서는 다른 내부 로직이 구현되어 있진 않았습니다)
의도를 더 드러내고 싶다면 `of`, `from` 대신 다른 이름을 붙일 수도 있습니다.
단, 정적 팩토리 메서드를 사용하는 경우 일반적으로 생성자를 private으로 선언하기 때문에 상속을 할 수 없습니다.
부모 클래스가 될 수 없다는 의미입니다. private 생성자에 접근할 수 없기 때문입니다.
그러나 이는 `조합`으로 해결이 가능합니다. 간단히 말해서, 멤버로 가지면 정적 팩토리 메서드를 가진 클래스를 사용할 수 있습니다.
상속보다는 조합을 사용하자에 대한 글을 참고하면 좋습니다.
2. 불변 객체의 재사용성이 높을 때
캐싱을 통해 한 번 생성된 객체를 재사용할 수 있습니다.
덕분에, 매번 new를 하지 않아도 돼서 코드를 최적화할 수 있겠군요!
Integer의 valueOf 정적 팩토리 메서드 내부에서 캐싱이 이뤄지고 있습니다.
이를 활용해, 불변 클래스에서 캐싱을 통해 인스턴스를 관리한다면, 같은 값을 가진 객체의 수를 통제할 수 있겠습니다.
싱글턴도 같은 맥락으로 1개의 인스턴스 생성을 통제하고 있습니다.
public class RandomCard {
private static RandomCard instance = new RandomCard();
private RandomCard() {
}
public static RandomCard getInstance() {
if (instance == null) {
instance = new RandomCard();
}
return instance;
}
...
}
단, 같은 리소스를 재사용한다는 측면에서 동시성 이슈에 대한 고려도 필요합니다.
또한, 캐싱이 자주 이뤄지지 않는 상황에서 비효율을 고려해야 합니다. 재사용성이 되지 않는 계속해서 들고 있거나, 이미 생성된 객체인지 확인하는 로직이 불필요하게 사용될 수 있습니다.
정리
객체 생성 방식
- 주 생성자와 부 생성자
- this 키워드로 부 생성자에서 주 생성자를 호출하자
- 정적 팩토리 메서드
- static 메서드로 생성자를 간접적으로 호출하자
사용 기준
- 생성자를 default로 사용하자
- 상황에 따라 정적 팩토리 메서드를 고려하자
- 다른 비즈니스 로직이 포함되어 있다는 의도를 드러낼 때
- 불변 객체를 재사용할 필요가 있을 때
'Java' 카테고리의 다른 글
상태를 공유하는 시나리오 테스트 DynamicTest (25) | 2024.04.01 |
---|---|
의존성 역전을 언제 쓰나요? (3) | 2024.03.25 |
돈은 머니머니해도 BigDecimal를 사용하자 (22) | 2024.03.18 |
도메인에 Record를 써도 될까요? (7) | 2024.03.04 |