2023. 2. 26. 23:24ㆍ자바
아이템 05 - 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
(1) 유연하지 않고, 테스트하기 어려운 클래스
- 정적 유틸리티를 잘못 사용한 예시
public class RacingGame {
private static final List<Car> cars;
private static final NumberGenerator numberGenerator = new RandomNumberGenerator();
private RacingGame() { // 객체 생성 방지}
// ... 생략
private static void start() {
cars.stream()
.filter(car -> car.canMove(**numberGenerator.generate()**))
.forEach(Car::move);
}
}
cf) 정적 유틸리티란, 정적 메서드와 정적 변수만 제공하는 것을 의미한다. 클래스의 본래 목적인 로직의 캡슐화에 초점을 두지 않고 비슷한 기능의 메서드나 상수를 모아서 캡슐화하는 것을 의미한다.
위의 클래스는 도메인을 설명하는 로직이라는 점 때문에 정적 유틸리티의 예시로 보기에 적절하지는 않으나, 크루들의 이해를 돕기 위해 자동차 경주 클래스를 정적 유틸리티의 형태로 변경했다.
- 싱글턴을 잘못 사용한 예시
public class RacingGame {
private final List<Car> cars;
private final NumberGenerator numberGenerator = new RandomNumberGenerator();
private RacingGame(...) {...}
private static final RacingGame INSTANCE = New RacingGame();
// ... 생략
private void start() {
cars.stream()
.filter(car -> car.canMove(**numberGenerator.generate()**))
.forEach(Car::move);
}
}
cf) 싱글턴 패턴: 불필요한 객체 생성을 막기 위해 객체를 하나만 생성하여 사용한다.
위의 두 코드는 유연하지 못하고 테스트하기 어렵다는 단점을 가지고 있다. 그 이유는 numberGenerator를 직접적으로 클래스 내에서 할당해주고 있기 때문이다. 이 때문에 자동차 게임마다 자동차를 움직일 수 있는지 판단하는 로직이 달라지게끔 설정할 수 없어진다. 또한 자동차가 갈 수 있는 지에 대한 테스트 과정을 돌려보기도 어렵기도 하다.
이를 해결하기 위한 간단한 방법으로는 numberGenerator 필드에서 final 한정자를 제거하고 다른 generator로 교체하는 방법(setter)을 생각해볼 수 있다. 하지만 이는 오류를 발생시킬 가능성도 높으며, 멀티쓰레드 환경에서는 쓸 수 없다.
따라서 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티나 싱글턴 방식이 적절하지 않다.
(2) 의존 객체 주입은 유연성과 테스트 용이성을 높여준다.
public class RacingGame {
private final List<Car> cars;
private final NumberGenerator numberGenerator;
public RacingGame(List<Car> cars, **NumberGenerator numberGenerator**) {
this.cars = cars;
**this.numberGenerator = numberGenerator;**
}
// ... 생략
private void start() {
cars.stream()
.filter(car -> car.canMove(**numberGenerator.generate()**)
.forEach(Car::move);
}
}
RacingGame은 딱 하나의 NumberGenerator를 사용하지만 자원이 몇 개든, 의존 관계가 어떻든 상관없이 잘 작동한다.
또한 불변을 보장하여 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있기도 하다.
이러한 의존 객체 주입은 생성자, 정적 팩터리, 빌더 모두에 똑같이 응용할 수 있다.
의존 객체 주입은 유연성과 테스트 용이성을 개선해주기는 하지만 의존성이 매우 많은 큰 프로젝트에서는 코드를 어지럽게 만들기도 한다. Spring과 같은 의존 객체 주입 프레임워크를 사용하면 이런 부분을 정리해줄 수 있다.
핵심 정리
<aside> 💡 클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 이 자원들을 클래스가 직접 만들게 하는 것도 좋지 못하다.
</aside>
<aside> 💡 대신 필요한 자원을 생성자에 넘겨줘서 의존 객체 주입을 사용하면 클래스의 유연성, 재사용성, 테스트 용이성을 개선해준다.
</aside>
cf) 팩토리 메서드 패턴
팩토리란 호출될 때마다 특정 타입의 객체를 반복적으로 생성해주는 객체를 의미한다.
간단한 예시를 들어 팩토리 메서드 패턴을 보도록 하자.
Interface
public interface Shape {
void draw();
}
Class
public class Circle implements Shape {
@Override
void draw() {
System.out.print("Circle");
}
}
public class Rectangle implements Shape {
@Override
void draw() {
System.out.print("Rectangle");
}
}
public class Square implements Shape {
@Override
void draw() {
System.out.print("Square");
}
}
Factory
public class ShapeFactory {
// 팩토리 메서드 -> 객체 생성 후 반환
public Shape getShape(String shapeType) {
if (shapeType == null) {
return null;
}
if (shapeType.equals("Circle") {
return new Circle();
}
if (shapeType.equals("Rectangle") {
return new Rectangle();
}
if (shapeType.equals("Square") {
return new Square();
}
return null;
}
}
위의 예시처럼 Factory를 사용하여 Shape을 구현한 객체들의 생성을 관리할 수 있다. 이는 객체 생성에 관한 확장을 쉽게 구현할 수 있다는 이점이 있다.
위에서도 언급을 했지만 이러한 팩토리 메서드 패턴에 의존관계 주입을 사용하게 되면 팩터리 타입 매개 변수를 제한할 수 있게 된다.
위의 예시에서는 단순히 String을 통해 값을 입력받았지만 아래와 같이 한정적 와일드 카드 타입을 활용해 Circle을 구현한 클래스만 전달받을 수 있도록 팩토리의 매개 변수를 제한할 수 있다.
변경된 Factory
public class ShapeFactory {
// 팩토리 메서드 -> 객체 생성 후 반환
public Shape getShape(Supplier<? extends Shape> shape) {
return shape.get();
}
}
'자바' 카테고리의 다른 글
아이템 15, 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2023.03.02 |
---|---|
자바, 객체야~ 일해라! (0) | 2023.02.27 |
자바, 싱글톤 패턴 실습 (0) | 2022.09.03 |
자바(3), 인터페이스 (0) | 2022.08.31 |
자바(2), 객체 지향 프로그래밍 (0) | 2022.08.29 |