[Spring] 의존성 주입(DI) 알아보기 (1)

송어·2023년 11월 27일
0

SpringStudy

목록 보기
2/3

이번에 자바의 객체 지향에 대해 다시 살펴보면서 의존성 주입의 개념, 필요성 등에 대해서도 다시 한번 공부해보고 있다. 이전에 스프링을 처음 사용해보면서 의존성 주입을 사용할 줄은 알았지만 내부 작동 방식이나 라이프 사이클은 구체적으로 알지 못한 채 사용하다보니 스프링 프레임워크 자체를 공부해야할 필요성을 느끼게 되었다.
스프링 프레임워크가 가진 기능은 매우 다양하다. 하지만 스프링 공식문서에도 핵심 기술이라고 나와있듯, 의존성 주입(DI)과 관련한 기능은 꼭 알고 넘어가야 한다고 생각했다. 이번 포스팅에선 의존성이라는 개념과 자바 코드로 직접 의존성 주입을 해볼 것이다.

객체지향(OOP)에서 바라본 의존성

의존성이란 서로 다른 객체 사이의 관계에서, 하나의 객체가 다른 객체를 사용하고 있는 상태를 말한다. 코드 상에서 바라본다면 두 모둘 간의 연결을 의미한다.

먼저 코드를 통해 알아보자

public class GameController {

    private MapleStory game = new MapleStory();

    public void run() {
        System.out.println("game = " + game);
        game.monsterKill();
        game.levelUp();
    }
}

게임 컨트롤러로 메이플스토리 게임을 작동시키는 코드이다. GameController의 멤버 변수 타입으로 MapleStory 클래스가 존재하는 것을 알 수 있다. main 클래스에서 게임 컨트롤러를 실행한다면 메이플스토리 게임이 실행되는 것을 확인할 수 있다.

그런데 내가 다른 게임을 하고싶다면 어떻게 해야할까? 예를 들어, 메이플스토리가 아닌 배틀그라운드 게임을 하고싶다고 가정한다면 현재 GameController 클래스의 코드 대부분을 변경해야 한다.
이 코드의 문제점은 크게 2가지이다.

  • MapleStory 클래스 없다면 GameController 클래스 자체를 정의할 수 없다.
  • 다른 게임을 실행하고 싶다면 GameController 클래스 내부 구조를 대부분 바꿔야 한다.

이것이 한 객체가 다른 객체를 의존하고 있는 상태, 다시 말해 GameController 클래스가 MapleStory 클래스에 의존하고 있는 상태이다. 따라서 GameController 클래스가 MapleStory 클래스에 의존성이 존재한다고 말할 수 있다. 이 중에서도 GameController 클래스는 다른 객체에 강한 의존성을 가지고 있어 강한 결합상태라고 볼 수 있다.

강한 결합

객체의 의존 관계에서 강한 결합이란, 어떤 객체가 다른 객체에 강한 의존성을 가지고 있다는 것을 의미한다.

public class GameController {

    private MapleStory game = new MapleStory();

    public void run() {
        System.out.println("game = " + game);
        game.monsterKill();
        game.levelUp();
    }
}

해당 코드에서 GameController 클래스는 MapleStory 클래스를 직접 참조하고 있다. GameController는 MapleStory게임만 실행시킬 수 있고, 실행시키고 싶은 게임을 바꾼다면 어떻게 될까?

public class GameController {

    private BattleGround game = new BattleGround(); // 변경점 1
    
    public void run() { // 변경점 2
        System.out.println("game = " + game);
        game.userKill();
        game.getChicken();
    }
 }

우선 멤버변수 타입이 BattleGround로 변경되고 run()메서드도 변경되게 된다. 이러한 상태를 강한 의존성을 가지고 있는 상태, 즉 강한 결합 상태라고 말하고, 강한 결합 상태인 의존 관계는 코드 간 변경에 있어 많은 변경을 요구해 유지 보수가 힘들어진다. 강한 결합도를 낮추고 유지 보수를 용이하게 변경하려면 객체지향의 특징인 다형성을 활용해야 한다.

물론 그냥 코드를 변경해 유지보수하면 되지 않나..? 라고 생각하는 사람들도 있을 것이다. 하지만 관리해야할 클래스가 하나 둘 늘어나고 서비스의 규모가 커질수록 이러한 객체의 의존 관계가 다양헤지고, 작은 코드 하나의 변경이 이러한 강한 결합도로 인해 크나큰 영향을 불러올 수가 있다.

느슨한 결합

느슨한 결합이란 클래스간 결합도를 낮춰 확장성과 유지 보수를 용이하도록 조정한 것을 의미한다. 느슨한 결합은 구현 클래스를 직접 참조하는 것과 달리 다형성을 활용한 '추상화'에 의존하는 것이다. 즉, 구현 클래스에 직접 의존하지 않고, 인터페이스에 의존하도록 설계해 역할-구현을 명확히 분리하고 클라이언트 코드에 영향을 줄여 의존도를 낮추는 것이다.

public interface GameProgram {
    void useGame();
}
public class BattleGround implements GameProgram {

    @Override
    public void useGame() {

        // 이하 생략

    }
}
public class GameController {

    private GameProgram game = new BattleGround(); // 변경점 1

    public void run() {
        System.out.println("game = " + game);
        game.useGame();
    }
}

결합도를 낮추고 변경에 용이하도록 코드를 조정하기 위해 GameProgram 인터페이스를 만들었다. BattleGround클래스와 MapleStory클래스는 GameProgram의 구현체이고, 현재 BattleGround 클래스를 참조해 게임을 작동시키도록 설계했다.

강한 결합과는 달리 게임의 종류에 따라 GameController클래스에서 변경될 코드는 GameProgram에 인스턴스를 주입하는 부분 뿐이다.

이렇게 느슨한 결합은 코드의 변경점을 크게 줄여 유지보수성을 향상시킬 수 있다. 하지만 GameProgram클래스는 구현체까지 알고 있어야 한다. 개발자가 직접 의존성을 주입해야하는 구조에서는 벗어나지 못했다. 개발자는 비즈니스 로직을 관리하면서 또한 객체별 의존 관계도 하나하나 신경써야하는 구조인 것이다.

여기서 의존성 주입(DI)이라는 개념이 등장한다.

의존성 주입(DI)

의존성 주입의 기본적인 의미는 외부에서 클라이언트에게 서비스를 제공(주입)하는 것이다.
다시말해, 클래스 간의 의존성을 외부에서 제어하는 것을 말한다. 현재 코드 상의 의존성을 외부에서 주입받도록 수정하기 위해 애플리케이션의 의존성을 담당하는 클래스인 GameBox 클래스를 생성해 의존성을 주입받는 방식을 구현해보자.

public class GameConfig {

    public GameController gameController() {
        return new GameController(gameProgram());
    }

    public GameProgram gameProgram() {
        return new MapleStory();
    }
}
public class GameController {

    private GameProgram game;

    public GameController(GameProgram game) {
        this.game = game;
    }

    public void run() {
        System.out.println("game = " + game);
        game.useGame();
    }
}
public class mainApp {
    public static void main(String[] args) {
        GameConfig gameConfig = new GameConfig();
        GameController controller = gameConfig.gameController();

        controller.run();
    }
}

애플리케이션의 의존성을 관리하는 클래스인 GameConfig를 따로 생성해 의존성 주입 및 제어만 담당하도록 정했다. 그리고 메인 클래스에서 GameConfig 클래스를 통해 의존성을 주입받을 수 있도록 설계했다.


이제 GameController는 외부에서 GameProgram의 구현체를 주입받음으로써 조금 더 느슨한 결합이 가능해졌다. 이제 구현 클래스가 추가되거나 바뀌어도 GameController 내부에선 전혀 변화가 일어나지 않게 되었다. 의존성을 담당하는 GameConfig 클래스만 변경해주면 되는 것이다. 이로써 역할-구현 클래스를 명확히 구분하고 클라이언트 객체는 비즈니스 로직만 실행할 수 있도록 권한을 줄였다.

프로그램상 의존성 제어 흐름을 GameBox 클래스가 장악하게 되었다.

현재는 순수한 자바코드만으로 의존성을 주입하는 과정을 설계했다.

스프링은 이러한 의존성 주입을 대신 하고 제어할 수 있는 핵심 기능이 있다. 이제 스프링을 사용해 의존성을 관리하도록 코드를 수정해보자.

@Configuration
public class GameConfig {

    @Bean
    public GameController gameController() {
        return new GameController(gameProgram());
    }

    @Bean
    public GameProgram gameProgram() {
        return new MapleStory();
    }
}
public class mainApp {
    public static void main(String[] args) {
//        GameConfig gameConfig = new GameConfig();
//        GameController controller = gameConfig.gameController();

        ApplicationContext ac =
                new AnnotationConfigApplicationContext(GameConfig.class);

        GameController bean = ac.getBean(GameController.class);

        bean.run();
    }
}

GameConfig클래스에 @Configuration, @Bean어노테이션을 붙이고 main클래스에
ApplicationContext ac = new AnnotationConfigApplicationContext(GameConfig.class);코드를 추가했다. 다음 포스팅에선 왜 이런 방식으로 코드를 변경했는지와, 스프링에서 제공하는 의존성 주입을 사용하기 위한 어노테이션 및 코드에 대해 다루겠다.

Reference

https://9hyuk9.tistory.com/6
https://citronbanana.tistory.com/8

0개의 댓글

관련 채용 정보