이펙티브 자바 아이템1(생성자 대신 정적 팩터리 메서드를 고려하라)를 읽고 사다리 미션에 적용해보며 학습한 내용들을 정리해보자.
사다리 생성 미션을 진행하며, 다음과 같은 궁금증이 들었다.
Player
의 일급 컬렉션 Players
는, 생성자의 파라미터로 List<Player> players
를 받는게 좋을까, List<String> playerNames
를 받는게 좋을까?
// 1. List<Player> players 받기
public Players(List<Player> players) {
this.players = players;
}
// 2. List<String> playerNames 받기
public Players(List<String> playerNames) {
List<Player> players = playerNames.stream()
.map(Player::new)
.collect(Collectors.toUnmodifiableList());
this.players = players;
}
playerNames를 List로 바꿔주는 것도 Players의 책임같은데..
2번처럼 진행하자니 생성자가 길어지고,
생성자에서는 파라미터로 해당 클래스의 필드 값을 받아, 필드값을 검증하고 초기화하는 역할만을 진행하고 싶다!
그럴 때 바로 정적 팩토리 메서드를 사용하면 된다!
정적 팩토리 메서드란?
정적 팩토리 메서드란 생성자 대신 객체를 생성해주는 정적(static) 함수를 뜻한다.
다음은 LocalDate
클래스의 정적 팩터리 메서드 of
이다.
public static LocalDate of(int year, int month, int dayOfMonth) {
YEAR.checkValidValue(year);
MONTH_OF_YEAR.checkValidValue(month);
DAY_OF_MONTH.checkValidValue(dayOfMonth);
return create(year, month, dayOfMonth);
}
of함수는 LocalDate 객체를 리턴해준다.
즉, 우리는 new LocalDate(...)
처럼 직접 생성자를 통해 객체를 생성하는 대신,
of라는 스태틱 메서드를 사용하여 객체를 생성할 수 있다.
위와 같이 생성자가 아닌, 스태틱 메서드를 통해 객체를 생성하는 방법을 정적 팩토리 메서드라 한다.
정적 팩토리 메서드 도입기 in 일급 컬렉션
포스팅 가장 윗 부분에서 나는 다음과 같은 고민을 했다.
Player의 일급 컬렉션 Players는, 생성자의 파라미터로 List<Player> players를 받는게 좋을까, List<String> playerNames를 받는게 좋을까?
그리고 정적 팩토리 메서드에 대해 알아봤다.
그러면, 정적 팩토리 메서드를 도입해 해당 고민을 해결해보자!
생성자 대신 정적 팩토리 메서드에 파라미터로 List<String> playerNames 를 받자!
private Players(List<Player> players) {
validate(players);
this.players = new ArrayList<>(players);
}
public static Players valueOf(List<String> playerNames) {
List<Player> players = playerNames.stream()
.map(Player::new)
.collect(Collectors.toUnmodifiableList());
return new Players(players);
}
위 코드는 다음과 같은 특징을 가진다.
- 생성자의 파라미터로 해당 클래스의 필드 값인 List<Player> players 를 받는다.
- 생성자는 1.필드값을 검증하고 2.필드를 초기화 하는 역할만 가진다.
- 정적 팩토리 메서드(valueOf)의 파라미터로 List<String> playerNames 를 받는다.
- Player 객체 생성 책임은, 그의 일급 컬렉션인 Players가 가진다.
일급 컬렉션을 사용함으로써 나의 모든 고민이 해결되었다!
그러면, 정적 팩토리 메서드는 일급 컬렉션에서만 사용 가능할까?
정답은 NO 이다!
나는 객체 생성시 복잡한 작업을 수행해야 할 경우라면, 해당 책임을 생성자에서 분리하기 위해 정적 팩토리 메서드를 사용하였다.
그게 꼭 일급 컬렉션이 아니라도 말이다!
정적 팩토리 메서드 도입기 in enum (1)
그렇게 고민을 해결하고 순조롭게 미션을 진행하던 중, 또다른 난관에 부딪혔다!
enum의 생성자는 private만 허용된다.
즉, 외부에서 생성자를 호출할 수 없다.
그런데 나의 경우, 사다리 발판의 유무 여부를 Point 라는 객체로 정의했다.
그리고 그 사다리 발판 유무는 랜덤하게 결정된다. (Point가 EXIST인지 NOT_EXIST인지 랜덤하게 결정된다)
지난 포스팅에서도 작성한 것 처럼, 랜덤하게 객체의 상태가 결정되면 테스트하기 어려운 상태가 된다.
테스트하기 쉽게 만들기 위해선, 전략 패턴을 사용해야 한다.
그래서 나는 다음과 같이 전략 패턴을 도입하고 싶었다.
1. PointGenerator 인터페이스를 정의한 후
2. 이를 Point에 주입해
3. PointGenerator를 활용해 사다리 발판 유무를 결정하여
4. 그에 맞는 Point 객체를 생성하기
그런데.. Point에 PointGenerator를 어떻게 주입해주지? enum의 생성자는 private인데?
우리는 위에서 생성자를 사용하지 않고 객체를 생성하는 방법을 익혔다. (정적 팩토리 메서드)
(사실 새로운 객체를 생성하는건 아니다. 이는 enum의 싱글턴 특성에 대해 알아보기 바란다)
이를 활용해보자.
public enum Point {
EXIST,
NOT_EXIST;
public static Point choosePoint(final Point previousPoint, final PointGenerator pointGenerator) {
if (previousPoint == EXIST) {
return NOT_EXIST;
}
return choosePoint(pointGenerator);
}
public static Point choosePoint(final PointGenerator pointGenerator) {
return pointGenerator.generate();
}
/***/
}
public interface PointGenerator {
Point generate();
}
정적 팩토리 메서드를 사용해서 깔끔하게 내 고민을 해결할 수 있었다!
정적 팩토리 메서드 도입기 in enum (2)
다음은 사용자가 "y"를 입력할 시 HitCommand.YES 를,
"n"을 입력할 시 HitCommand.NO 를 반환하기 위해 작성된 코드이다.
public enum HitCommand {
YES("y", true),
NO("n", false);
private final String command;
private final boolean isHit;
HitCommand(String command, boolean isHit) {
this.command = command;
this.isHit = isHit;
}
public static HitCommand from(final String command) {
return Arrays.stream(HitCommand.values())
.filter(hitCommand -> command.equals(hitCommand.command))
.findAny()
.orElseThrow(IllegalArgumentException::new);
}
/***/
}
...
HitCommand command1 = HitCommand.from("y"); // YES
HitCommand command2 = HitCommand.valueOf("YES"); //YES
from은 String 형태의 파라미터를 전달받아, 적절한 HitCommand 객체를 반환하는 정적 팩토리 메서드이다.
그리고 java에서 기본적으로 제공하는 valueOf라는 메서드도 확인할 수 있다.
valueOf는 enum의 이름을 파라미터로 전달받아, enum 객체를 반환하는 정적 팩토리 메서드이다.
정적 팩토리 메서드 도입기 in 응답 객체
public class LadderResponse {
private final List<List<Boolean>> ladder;
private final List<String> players;
private final List<String> prizes;
private LadderResponse(List<List<Boolean>> ladder, List<String> players, List<String> prizes) {
this.ladder = ladder;
this.players = players;
this.prizes = prizes;
}
public static LadderResponse of(final Ladder ladder, final Players players, final Prizes prizes) {
List<List<Boolean>> ladderResponse = ladder.getLines().stream()
.map(Line::getPoints)
.map(LadderResponse::convertPointsToValues)
.collect(Collectors.toUnmodifiableList());
List<String> playerNames = players.getPlayerNames();
List<String> prizeValues = prizes.getPrizeValues();
return new LadderResponse(ladderResponse, playerNames, prizeValues);
}
/***/
}
개발자는 코드로 소통하는 법.
정적 팩토리 메서드 안에 있는 내용들이 생성자에 들어가있다면?
고도비만 생성자가 되었을 것 같다.
하지만 정적 팩토리 메서드를 사용하니
생성자는 필드 값을 파라미터로 전달받아 필드를 초기화 해주는 역할만 한다.
public class TotalResultResponse {
private final Map<String, String> playerToPrize;
private TotalResultResponse(final Map<String, String> playerToPrize) {
this.playerToPrize = new LinkedHashMap<>(playerToPrize);
}
public static TotalResultResponse from(LadderGameResult ladderGameResult) {
Map<Player, Prize> result = ladderGameResult.getPlayerToPrize();
Map<String, String> playerToPrize = new LinkedHashMap<>();
result.forEach((player, prize) -> playerToPrize.put(player.getName(), prize.getValue()));
return new TotalResultResponse(Collections.unmodifiableMap(playerToPrize));
}
/***/
}
필드가 여러개가 아닌 하나일 때도, 이렇게 깔끔하게 역할을 분리할 수 있다.
마무리
이렇게 정적 팩토리 메서드의 활용법은 무궁무진하다!
본인의 생성자가 조금 뚱뚱해 진다면,
혹은 생성자를 private으로 두고싶은데 객체 생성시 어떠한 작업을 하고싶다면
정적 팩토리 메서드를 활용해보자
'우테코 5기 > 레벨1' 카테고리의 다른 글
[체스 미션] 클래스 abstract / final로 만들기 (6) | 2023.03.29 |
---|---|
[체스 미션] 상속 대신 전략패턴 도입하기 (4) | 2023.03.28 |
전략 패턴을 통한 테스트 (2) | 2023.02.17 |
View단에서 도메인 객체를 생성해 return하는게 좋을까, 아니면 입력값을 그대로 return하는게 좋을까? (3) | 2023.02.17 |
입력값 검증을 InputView에서 진행할까, Domain에서 진행할까? (1) | 2023.02.17 |