Board를 어떻게 만드는지에 대한 제어를 하고 싶으시다면 제어하고 싶은 영역의 의존성을 역전시켜 보면 좋을 것 같습니다.
체스 미션을 수행하던 중 이런 리뷰를 받았었습니다.
왜 의존성 역전을 제안 하셨을까? 또 어떤 장점이 있을까?
기존 코드는 이러했습니다.
public class Board {
private static final List<String> INITIAL_BOARD = List.of(
"RNBQKBNR",
"PPPPPPPP",
"........",
"........",
"........",
"........",
"pppppppp",
"rnbqkbnr"
);
private final Map<Position, Piece> board;
private int turnCount;
private Board(Map<Position, Piece> board, int turnCount) {
this.board = board;
this.turnCount = turnCount;
}
// 팩토리 메서드 1
public static Board createInitialBoard() {
return new Board(generateBoard(INITIAL_BOARD), 0);
}
// 팩토리 메서드 2
public static Board createCustomBoard(List<String> customBoard, int turnCount) {
return new Board(generateBoard(customBoard), turnCount);
}
...
}
`Board`를 생성하는 두 가지 방식이 있습니다.
- `createInitialBoard` : `INITIAL_BOARD`로 초기화
- `createCustomBoard` : 원하는 Board 모양대로 생성
사실, 위 코드에는 문제가 있습니다.
테스트에서 사용하는 생성 로직이 프로덕션 코드에 있다는 점입니다.
서비스에서는 createInitialBoard 로직만 사용되고, createCustomBoard 는 불필요한 코드였습니다.
테스트 환경에서만 사용될 코드가 운영환경에 남아있는 것은 좋지 않아 드린 리뷰였습니다. 테스트에서만 사용될 코드가 운영환경에 남아있지 않으면서 테스트하기 쉬운 환경을 조성할 수 있는 방법으로 의존성 역전을 말씀드린 것이었습니다. 물론 상황이 변하여 운영환경에서도 정적팩토리 메서드 혹은 생성자의 오버로딩이 충분히 의미 있게 쓰일 수 있게 된다면 고안하신 방법이 더 좋아질 수 있을 것 같습니다.
아하! 운영 환경에 테스트용 코드가 남아있지 않으면서, 테스트 하기 쉬운 환경을 만들 때 의존성 역전을 사용할 수 있겠구나!
의존 관계 역전 원칙(Dependency Inversion Principle)
의존성 역전이 무엇인지 먼저 짚어보겠습니다. 객체 지향의 SOLID 원칙 중 하나입니다.
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 되며, 저수준 모듈이 고수준 모듈에 의존해야 한다는 것
즉, 하위 모듈에 의존하지 않고 상위 모듈인 인터페이스나 추상 클래스에 의존해야 하는 것을 의미합니다.
의존성을 역전시키면 모듈 간 관계를 느슨하게 유지할 수 있습니다. DIP를 지키면, 변경 사항이 생기더라도 기존 코드를 수정하지 않을 수 있습니다.
코드로 살펴보는 의존성 역전
기존 코드에서 의존성을 역전시켜 운영 환경에 테스트용 코드를 지워보겠습니다.
public interface BoardFactory {
Board generate();
...
}
`Board`의 생성 규약을 정의한 인터페이스(상위 모듈)입니다.
public class InitialBoardFactory implements BoardFactory {
private static final List<String> INITIAL_PIECES = List.of(
"RNBQKBNR",
"PPPPPPPP",
"........",
"........",
"........",
"........",
"pppppppp",
"rnbqkbnr"
);
@Override
public Board generate() {
return new Board(generatePieces(INITIAL_PIECES), 0);
}
}
인터페이스를 구체화한 `InitialBoardFactory` 클래스(하위 모듈)입니다.
BoardFactory boardFactory = new InitialBoardFactory();
Board board = boardFactory.generate();
`Board`는 상위 모듈인 `BoardFactory에만 의존하고 있고, 구체화된 생성 로직에 대해서는 알지 못합니다.
생성 로직을 변경하고 싶다면 `new InitialBoardFactory()` 만 바꿔주면 되기 때문에 **개방-폐쇄 원칙(OCP)**을 만족시킬 수도 있습니다.
개방-폐쇄 원칙(Open-Closed Principle)
확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다. 즉, 기존 코드의 수정 없이 기능 확장이 쉬워야 한다는 원칙
public class CustomBoardFactory implements BoardFactory {
private final List<String> pieces;
private final int turnCount;
public CustomBoardFactory(List<String> pieces, int turnCount) {
this.pieces = pieces;
this.turnCount = turnCount;
}
@Override
public Board generate() {
return new Board(generatePieces(pieces), turnCount);
}
}
반면, 테스트 용도로 사용되는 `CustomBoardFactory`는 필요시에, 테스트 코드에 재정의하여 사용할 수 있습니다.
그림으로 의존 관계를 나타내면 다음과 같습니다.
@FunctionalInterface
추상 메서드가 오직 1개만 존재한다면 함수형 인터페이스를 사용할 수 있습니다.
default, static 메서드 숫자와는 관련이 없습니다.
함수형 인터페이스는 람다식을 쓸 수 있다는 장점이 있습니다.
FunctionalInterface | Descripter | Method |
Predicate | T -> boolean | boolean test(T t) |
Consumer | T -> void | void accept(T t) |
Supplier | () -> T | T get() |
Function<T, R> | T -> R | R apply(T t) |
Comparator | (T, T) -> int | int compare(T o1, T o2) |
Runnable | () -> void | void run() |
Callable | () -> T | V call() |
참고 : https://bcp0109.tistory.com/313
사용 방법은 클래스 레벨에 `@FunctionalInterface` 애너테이션을 붙여주면 됩니다.
@FunctionalInterface
public interface BoardFactory {...}
애너테이션을 붙이지 않아도 동일하게 동작하지만, 가독성 측면에서 붙여주는 것을 권장합니다.
애너테이션을 붙이면 사용이 가능한지 컴파일 시점에서 확인할 수도 있습니다. 사용 불가능한 경우라면 예외를 띄워줍니다.
위 예시 코드에서 추상 메서드가 1개 존재하므로, 함수형 인터페이스를 사용할 수 있습니다.
BoardFactory boardFactory = () -> new Board(new HashMap<Position, Piece>(), 0);
Board board = boardFactory.generate();
정리
- 의존성 역전을 사용하면 좋을 때
- `운영 환경에 테스트용 코드가 남아있지 않으면서`, `테스트 하기 쉬운 환경`을 만들 때
- `OCP`의 이점을 챙기고 싶을 때
- 함수형 인터페이스와 결합하여 더욱 간편하게 사용할 수 있음
'Java' 카테고리의 다른 글
상태를 공유하는 시나리오 테스트 DynamicTest (25) | 2024.04.01 |
---|---|
돈은 머니머니해도 BigDecimal를 사용하자 (22) | 2024.03.18 |
객체 생성 방식에 기준이 있나요? (28) | 2024.03.11 |
도메인에 Record를 써도 될까요? (7) | 2024.03.04 |