Effective Java

Effective Java | Item 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

이진유진 2024. 4. 23. 16:57
반응형
Effective Java 3/E 판을 읽고 정리한 기록입니다.

사용하는 자원에 따라 동작이 달라지는  클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다. 

정적 유틸리티 클래스나 싱글톤 방식은 모두 객체의 생성과 사용을 제한하거나 제어하는 방식이지만, 

사용하는 자원에 따라 동작이 달라지는 클래스에는 적합하지 않습니다. 

 

이유 - 정적 유틸리티 클래스

정적 유틸리티 클래스는 주로 상태를 가지지 않고, 동일한 동작을 수행하는 메서드를 제공하는 데 사용됩니다. 

따라서 사용하는 자원에 따라 동작이 달라지는 경우에는 정적 유틸리티 클래스로 구현하기 어렵습니다. 

 

정적 유틸리티 클래스의 메서드는 주로 인자로 전달된 데이터를 기반으로 동작하기 때문에, 

동작이 자원에 따라 달라지는 경우, 해당 데이터를 인자로 전달하여 동작을 제어하기 어렵습니다. 

 

예시코드

public class MathUtils {
    // 생성자를 private으로 선언하여 외부에서 인스턴스화를 방지
    private MathUtils() {
        // 유틸리티 클래스이므로 인스턴스 생성 불가능하게 함
    }

    // 두 정수의 합을 반환하는 정적 메서드
    public static int add(int a, int b) {
        return a + b;
    }

    // 두 정수의 차를 반환하는 정적 메서드
    public static int subtract(int a, int b) {
        return a - b;
    }

    // 두 정수의 곱을 반환하는 정적 메서드
    public static int multiply(int a, int b) {
        return a * b;
    }

    // 두 정수의 나누기를 반환하는 정적 메서드
    public static double divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("Divisor cannot be zero");
        }
        return (double) a / b;
    }
}

 

이유 - 싱글톤 패턴

싱글톤 패턴은 어플리케이션 전역에서 단일 인스턴스를 공유하기 위해 사용됩니다. 

그러나 사용하는 자원에 따라 동작이 달라지는 경우에는 단일 인스턴스가 공유되어야 할 필요가 없을 수 있습니다. 

 

또한, 싱글톤 패턴은 상태를 유지하는데 사용되므로, 

상태가 자원에따라 동적으로 변하는 경우, 싱글톤 패턴으로 구현하기 어렵습니다. 

 

사용하는 자원에 따라 동작이 달라지는 클래스는 객체 생성과 사용을 동적으로 제어할 수 있는 방식을 사용하는 것이 적합합니다.
이를 위해 의존 객체 주입(Dependency Injection) 패턴이나 팩토리 메서드 패턴 등이 사용될 수 있습니다. 
이러한 패턴들은 객체 생성과 사용을 외부로 위임하고, 
필요에 따라 동적으로 객체를 생성하거나 교체할 수 있도록 유연성을 제공합니다. 

 

의존 객체 주입(Dependency Injection)

 

객체지향 프로그래밍에서 객체 간의 의존성을 외부에서 주입하는 디자인 패턴입니다. 

이 패턴은 객체가 필요로 하는 의존 객체를 외부에서 주입받아 사용하도록 하는 것을 목적으로 합니다. 

이를 통해 의존 객체의 생성과 사용을 분리함으로써 코드의 유연성과 테스트 용이성을 향상시킬 수 있습니다. 

의존 객체 주입 방법

생성자 주입(Constructor Injection)

객체의 생성자를 통해 의존 객체를 주입하는 방식입니다. 

생성자를 통해 의존성을 명시적으로 전달하므로 클래스의 의존성이 명확하게 드러나며, 클래스의 인스턴스화 시점에 의존성이 결정됩니다. 

 

public class MyClass {
    private MyDependency dependency;

    // 생성자를 통한 의존 객체 주입
    public MyClass(MyDependency dependency) {
        this.dependency = dependency;
    }

    public void doSomething() {
        // 의존 객체를 활용하여 작업 수행
        dependency.performTask();
    }
}

 

세터 주입(Setter Injection)

객체의 세터 메서드를 통해 의존 객체를 주입하는 방식입니다. 

주입할 의존 객체를 설정하는 메서드를  제공하여 외부에서 주입할 수 있습니다. 

이 방식은 선택적인 의존성을 처리할 때 유용하며, 

객체 생성 후에도 의존성을 변경할 수 있습니다. 

public class MyClass {
    private MyDependency dependency;

    // 세터 메서드를 통한 의존 객체 주입
    public void setDependency(MyDependency dependency) {
        this.dependency = dependency;
    }

    public void doSomething() {
        // 의존 객체를 활용하여 작업 수행
        dependency.performTask();
    }
}

 

인터페이스 주입(Interface Injection)

의존 객체를 주입하기 위한 인터페이스를 정의하고, 

해당 인터페이스를 구현한 클래스를 주입하는 방식입니다. 

이 방식은 직접적인 클래스 의존성을 갑추고, 런타임에 다양한 구현체를 주입하여 사용할 수 있습니다. 

public interface DependencyInjector {
    MyDependency getDependency();
}

// 의존 객체를 생성하는 구현체
public class DependencyInjectorImpl implements DependencyInjector {
    @Override
    public MyDependency getDependency() {
        return new MyDependencyImpl();
    }
}

public class MyClass {
    private MyDependency dependency;

    // 인터페이스를 통한 의존 객체 주입
    public MyClass(DependencyInjector injector) {
        this.dependency = injector.getDependency();
    }

    public void doSomething() {
        // 의존 객체를 활용하여 작업 수행
        dependency.performTask();
    }
}

팩토리 메서드 주입(Factory Method Injection)

객체를 생성하는 팩토리를 생성자에 주입하여, 

필요할 때마다 해당 팩터리를 사용하여 객체를 동적으로 생성하는 방식입니다. 

이 방식은 의존 객체의 생성을 외부에 위임함으로써 객체 생성과 사용을 분리합니다. 

import java.util.function.Supplier;

public class MyClass {
    private Supplier<MyDependency> dependencyFactory;

    // 생성자를 통한 팩터리 메서드 주입
    public MyClass(Supplier<MyDependency> dependencyFactory) {
        this.dependencyFactory = dependencyFactory;
    }

    public void doSomething() {
        // 팩터리 메서드를 사용하여 의존 객체 생성
        MyDependency dependency = dependencyFactory.get();
        // 의존 객체를 활용하여 작업 수행
        dependency.performTask();
    }
}

 

대규모 프로젝트에서는 의존성이 많아질수록 코드를 복잡하게 만든다. 

의존 객체 주입은 유연성과 테스트 용이성을 향상하지만, 

대규모 프로젝트에서는 의존성이 많아질수록 코드를 복잡하게 만들 수 있습니다. 

 

이러한 문제를 해결하기 위해 의존 객체 주입 프레임워크가 등장했습니다. 

 

가장 널리 사용되는 세 가지 프레임워크는 대거(Dagger), 주스(Guice), 스프링(Spring)이 있습니다. 

 

대거(Dagger)

구글에서 개발한 의존 객체 주입 프레임워크로, 

컴파일 타임 의존성 주입(Compile-time Dependency Injection)을 지향합니다.

이는 코드를 컴파일하는 동안 의존성을 확인하고 코드를 생성하여 의존성을 주입합니다. 

이러한 방식은 런타임 오버헤드를 최소화하고 빠른 성능을 제공합니다. 

또한 코드를 보다 명확하게 만들어 유지보수성을 향상시킵니다. 

 

주스(Guice)

주스는 구글에서 개발한 의존 객체 주입 프레임워크로, 

런타임 의존성 주입(Runtime Dependency Injection)을 제공합니다. 

주스는 구성 파일을 사용하여 의존성을 관리하며, 

다양한 구현체를 제공하여 유연한 의존성 주입을 지원합니다. 

주스를 사용하면 객체 간의 결합도를 낮추고 코드를 보다 모듀로하하고 테스트 가능하게 만들 수 있습니다. 

 

스프링(Spring)

스프링은 자바 기반의 애플리케이션을 개발하기 위한 종합적인 프레임워크로, 

의존 객체 주입을 비롯한 다양한 기능을 제공합니다. 

스프링은 XML 또는 어노테이션을 사용하여 의존성을 관리하고, 

런타임에 의존 객체를 주입합니다. 

스프링의 핵심 개념 중 하나는 제어의 역전(Inversion of Control, IoC)으로, 

객체의 생명주기와 의존성 관리를 스프링 컨테이너가 관리합니다. 

이는 애플리케이션의 확장성과 유지보수성을 향상시키는데 도움을 줍니다. 

 

이러한 의존 객체 주입 프레임워크를 사용하면 의존성 관리를 효율적으로 수행할 수 있습니다. 
프레임워크는 보다 명확하고 유연한 코드를 작성할 수 있도록 도와주며, 
객체 간의 결합도를 낮추고 테스트 용이성을 향상시킵니다. 
따라서 대규모 프로젝트에서는 의존 객체 주입 프레임워크의 사용이 거의 필수적입니다. 

 

클래스가 내부적으로 하나 이상의 자원에 의존하고,
그 자원이 클래스의 동작에 영향을 준다면
싱글턴(Singleton)과 정적 유틸리티 클래스(static utility class)는 사용하지 않는 것이 좋습니다.
이러한 자원들을 클래스가 직접 만들게 해서도 안 됩니다.
대신 필요한 자원을 (혹은 그 자원을 만들어 주는 팩터리를) 생성자에 (혹은 정적 팩토리나 빌더에) 넘겨주는 것이 바람직합니다.
이러한 기법을 의존 객체 주입(Dependency Injection)이라고 합니다.
의존 객체 주입은 클래스의 유연성, 재사용성, 테스트 용이성을 향상시켜줍니다.

 

 

의존 객체 주입이 클래스의 유연성, 재사용성, 테스트 용이성을 개선하는 이유 

의존성 분리(Dependency Separation) 

의존 객체 주입을 사용하면 클래스가 외부 자원에 의존하는 것을 명시적으로 표현할 수 있습니다. 

이는 클래스의 역할과 책임을 명확하게 구분할 수 있도록 해줍니다. 

또한, 의존성이 클래스 내부에 감추어지지 않고 외부에 노출되어, 클래스 간의 결합도가 낮아지고 유연성이 증가합니다. 

유연한 구성(Flexible Configuration)

의존 객체 주입을 사용하면 필요에 따라 다양한 구현체를 주입하여 클래스의 동작을 변경할 수 있습니다. 

이는 클래스의 확장성과 유연성을 높여줍니다. 

예를 들어, 인터페이스를 통해 의존성을 주입하면 런타임에 다른 구현체를 주입하여 동작을 변경할 수 있습니다. 

단위 테스트 용이성(Unit Testing Ease)

의존 객체 주입을 사용하면 테스트할 때 모의 객체(mock object)를 주입하여 의존성을 대체할 수 있습니다. 

이를 통해 테스트의 격리성을 확보하고 의존성이 있는 코드를 단위 테스트하기 쉽습니다. 

또한, 테스트 시에 의존성을 주입하여 원하는 상태를 시뮬레이션하고 테스트할 수 있습니다. 

재사용성(Reusability)

의존 객체 주입을 사용하면 동일한 클래스를 여러 곳에서 재사용할 수 있습니다. 

외부에서 필요한 의존성을 주입하기 때문에 클래스는 자체적으로 의존성을 관리하지 않습니다. 

이는 코드의 중복을 줄이고 재사용성을 높여줍니다. 

확장성(Scalability)

의존 객체 주입을 사용하면 시스템이 성장하고 변경될 때 쉽게 대응할 수 있습니다. 

새로운 기능을 추가하거나 기존 기능을 변경할 때 클래스의 수정 없이 외부에서 의존성을 주입하여 동작을 변경할 수 있습니다. 

이는 시스템의 확장성을 높여줍니다. 


산군의 CTO 준혁님께서 선물해 주신 
Effective Java 5장을 공부해 봤습니다 :)
다들 즐거운 개발되세요 :> 
반응형