WEEK 6-3: IOC/DI, Single Pattern

ensalada.de.pollo·2025년 5월 16일

be

목록 보기
25/44

IOC(제어의 역전, Inversion Of Control)

객체의 생성과 관리 권한을 개발자가 아닌, Spring Container가 담당하는 것입니다. 개발자가 객체를 직접 생성하고 관리했지만, Spring에서는 컨테이너가 객체의 생성, 주입, 소멸을 관리합니다.

앞선 게시글에서도 이야기했듯이, 객체 간의 의존도, 결합도를 낮춰 유연한 코드를 만들기 위함입니다.

DI(의존성 주입, Dependency Injection)

Spring이 객체 간의 의존성을 자동으로 주입해주는 것을 의미합니다. 한 객체가 다른 객체를 사용할 때, 해당 객체를 직접 생성하지 않고 Spring이 주입을 해주는 방식입니다.

IOC에서 의존성 주입을 이야기하고 있듯이, DI는 IOC를 구현하는 방식 중 하나에 속합니다.

// 개발자가 객체를 직접 관리하는 경우

// Service 구현체
public class MyServiceImpl implements MyService {
    private MyRepository myRepository;

    // 의존성 주입
    public MyServiceImpl(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    @Override
    public void doSomething() {
        System.out.println("서비스 작업 실행");
        myRepository.queryDatabase();
    }
}

// Repository 구현체
public class MyRepositoryImpl implements MyRepository {
    @Override
    public void queryDatabase() {
        System.out.println("데이터베이스 쿼리 실행");
    }
}

public class MyApp {
    public static void main(String[] args) {
        MyRepository repo = new MyRepositoryImpl();
				// MyRepository repo2 = new MyRepositoryImplV2();
        MyService myService = new MyServiceImpl(repo);
				// MyService myService2 = new MyServiceImpl(repo2);
        myService.doSomething();
    }
}

// 새로운 Repository 구현체
public class MyRepositoryImplV2 implements MyRepository {
    @Override
    public void queryDatabase() {
        System.out.println("데이터베이스 쿼리 실행 V2");
    }
}

객체끼리 강한 결합을 하고 있는 것을 볼 수 있습니다. 여기서 만약 새로운 Service나 Repository가 생긴다면, 클라이언트측 코드에 영향을 미칠 것입니다.

// Spring Conatiner가 관리하는 경우
// Service 구현체
@Service
public class MyIocService implements MyService {
    
    private final MyRepository myRepository;

    // 생성자 주입(DI 적용)
    @Autowired
    public MyIocService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    @Override
    public void doSomething() {
        System.out.println("IOC 서비스 작업 실행");
        myRepository.queryDatabase();
    }
}

// Repository 구현체
@Repository
public class MyIocRepository implements MyRepository {

    @Override
    public void queryDatabase() {
        // 데이터베이스와 상호작용
        System.out.println("IOC 데이터베이스 쿼리 실행");
    }
}

// Spring Container 관리(IoC 적용)
@ComponentScan(basePackages = "com.example")
public class MyIocApp {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyIocApp.class);

        // Service 빈을 가져와서 실행
        MyService service = context.getBean(MyService.class);
        service.doSomething();
    }
}

// 새로운 Repository 구현체
@Repository
public class MyIocRepositoryV2 implements MyRepository {

    @Override
    public void queryDatabase() {
        // 데이터베이스와 상호작용
        System.out.println("IOC 데이터베이스 쿼리 실행 V2");
    }
}

구현 코드가 변경되어도 클라이언트 코드에 영향이 없는 것을 볼 수 있습니다. 다른 구현체를 구현하여 Bean으로 등록하면 자유롭게 변경이 가능합니다. 사실, 위의 코드는 @Repository로 등록된 빈이 중복되기 때문에 충돌이 발생해서 정상적으로 동작하지는 않습니다. Spring Container를 사용하면 코드가 간결해질 수 있다는 것을 보이는 코드이므로 충돌에 관한 것은 이후에 다루도록 하겠습니다.

Spring Container를 사용하는 경우 IOC, DI를 통해 객체 간의 결합도를 낮추고 유연한 설계를 가능하도록 합니다.

Singleton Pattern

클래스의 인스턴스(객체)가 오직하나만 생성되도록 보장하는 디자인 패턴입니다.

등장 배경

WebApplication의 경우 불특정 다수가 많은 요청을 보냅니다. 요청을 보낼 때마다 새로운 객체를 생성하고, 처리가 완료되면 소멸이 됩니다. 여기서 오버헤드가 발생할 수 있고, 메모리 낭비도 매우 심해질 것입니다.

만약, 객체 인스턴스가 하나만 생성된다면? 여러 요청에도 하나의 객체만 사용하기 때문에 자원을 절약할 수 있습니다.

문제점

public interface Singleton {
	void showMessage();
}

public class SingletonImpl implements Singleton {
    private static SingletonImpl instance;

    // private 생성자를 통해 외부에서 객체 생성을 방지
    private SingletonImpl() {}

    // public으로 설정하여 인스턴스가 필요하면
    // getInstance 메서드를 통해 인스턴스에 접근하도록 만듦
    public static SingletonImpl getInstance() {
        // 인스턴스가 없을 때만 생성
        if (instance == null) {
            instance = new SingletonImpl();
        }
        return instance;
    }

    @Override
    public void showMessage() {
        System.out.println(instance.toString());
    }
}

Singleton Pattern의 예시 코드입니다.
여기서, 문제점은 구현하기 위한 코드의 양이 많고, 구현 클래스에 의존을 해야합니다.

public class MainApp {
    public static void main(String[] args) {
       // 첫 번째 싱글톤 인스턴스 요청, 구현클래스.getInstance();
        Singleton instance1 = SingletonImpl.getInstance();
        instance1.showMessage(); // 인스턴스 주소값 출력

        // 두 번째 싱글톤 인스턴스 요청, 구현클래스.getInstance();
        Singleton instance2 = SingletonImpl.getInstance();
        instance2.showMessage(); // 인스턴스 주소값 출력
        
        // 다른 구현체로 바꾸려면 DIP, OCP 위반
        Singleton instance3 = SingletonImplV2.getInstance();
        instance3.showMessage();
    }
}

유연성이 떨어지기 때문에 안티패턴이라고 불리기도 합니다.

Spring의 Singleton Container

Singleton Pattern의 문제를 해결하면서 객체를 Singleton으로 관리합니다.
Spring Bean 또한 Singleton으로 관리되는 객체입니다.

그럼 새로운 객체는 아예 생성을 하지 않는 것일까?

기본적으로 Bean을 등록하는 방법이 Singleton입니다. 요청할 때마다 새로운 객체를 만들어서 반환하는 기능 또한 제공하기도 합니다.

주의점

객체의 인스턴스를 하나만 생성해서 공유하는 싱글톤 패턴의 객체는 Stateful하면 안됩니다.

왜?

public class StatefulSingleton {
    private static StatefulSingleton instance;
    
    // 상태를 나타내는 필드
    private int value;

    // private 생성자
    private StatefulSingleton() {}

    // 싱글톤 인스턴스를 반환하는 메서드
    public static StatefulSingleton getInstance() {
        if (instance == null) {
            instance = new StatefulSingleton();
        }
        return instance;
    }

    // 상태 변경 메서드
    public void setValue(int value) {
        this.value = value;
    }

    // 상태를 반환하는 메서드
    public int getValue() {
        return this.value;
    }
}
public class MainApp {
    public static void main(String[] args) {
        // 클라이언트 1: 싱글톤 인스턴스를 가져와서 상태를 설정
        StatefulSingleton client1 = StatefulSingleton.getInstance();
        client1.setValue(42);
        System.out.println("클라이언트 1이 설정한 값: " + client1.getValue());

        // 클라이언트 2: 동일한 싱글톤 인스턴스를 사용해 상태를 변경
        StatefulSingleton client2 = StatefulSingleton.getInstance();
        client2.setValue(100);
        System.out.println("클라이언트 2가 설정한 값: " + client2.getValue());

        // 클라이언트 1이 다시 값을 확인
        System.out.println("클라이언트 1이 다시 확인한 값: " + client1.getValue());
    }
}

위와 같은 코드를 작성했다고 해보겠습니다. main 함수에서 싱글톤 인스턴스를 가져와 상태를 설정하고 있는데, 사실상 같은 인스턴스를 사용하고 있는 것이기 때문에 value는 공유되는 필드입니다.

// 출력 결과
클라이언트 1이 설정한 값: 42
클라이언트 2가 설정한 값: 100
클라이언트 1이 다시 확인한 값: 100

이런 식으로, 필드를 공유하기 때문에 데이터의 불일치와 동시성 문제를 유발할 수 있습니다.

따라서, Spring Bean은 항상 Stateless로 설계를 해야합니다.

자료 및 코드 출처: 스파르타 코딩클럽

0개의 댓글