이펙티브 자바, 의존 객체 추입

2023. 2. 26. 23:24자바

728x90
반응형

아이템 05 - 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라


(1) 유연하지 않고, 테스트하기 어려운 클래스

  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) 정적 유틸리티란, 정적 메서드와 정적 변수만 제공하는 것을 의미한다. 클래스의 본래 목적인 로직의 캡슐화에 초점을 두지 않고 비슷한 기능의 메서드나 상수를 모아서 캡슐화하는 것을 의미한다.

위의 클래스는 도메인을 설명하는 로직이라는 점 때문에 정적 유틸리티의 예시로 보기에 적절하지는 않으나, 크루들의 이해를 돕기 위해 자동차 경주 클래스를 정적 유틸리티의 형태로 변경했다.

  1. 싱글턴을 잘못 사용한 예시
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();
		}
}
728x90
반응형