[공부한 것] 의존관계가 강하다? 의존성 주입? 생성자 주입?

별의개발자커비·2023년 10월 29일
20

우테코 도전기

목록 보기
21/37

개요

의존성 주입 DI, 생성자 주입, 필드 초기화, DIP 등 비슷하지만 다른 용어들을 1주차 PR 리뷰에서 볼 수 있었다
이거겠거니 하지말고🚫🚫 정확히 알고 가보자!


🔎 우선, 의존성이란?

의존성이 생기면 결합도가 높아진다. 이런 말을 한 번쯤 들어봤을 것이다.
무슨 의미일까?

현재 이 GameController의 start() 메소드가 실행되려면 Game 객체가 필요하다.

public class GameController {

    private Game baseballGame = new Game();

    private void start() {
        do {
            baseballGame = initGame();
            playGame(baseballGame);
        } while (!baseballGame.isEnd());
    }
 }   

이 때, 우리는 'GameController 객체는 Game 객체에 의존성을 갖는다'라고 이야기한다!


의존성 주입 없이, 강한 의존관계를 갖는 코드

그런데 위 코드가 강한 의존관계, 결합도가 높은 코드라고?

그렇다. 의존성 주입을 하지 않고 GameController 클래스 내에서 Game 객체를 직접 생성했기 때문이다!
의존성 주입이 무엇인지는 뒤에서 알아보고,

일단 이 코드가 발생시키는 문제점을 먼저 알아보려고 한다.


1. Game 객체 수정 시, GameController 클래스도 함께 수정해야 한다.

GameController 클래스 내에 Game 객체를 직접 생성하는 부분이 있는데,

Game baseballGame = new Game()
-----
Game baseballGame = Game.init()

만약 Game 객체 생성 방법이 이런식으로 변경된다면 GameController 클래스도 이렇게 함께 수정해야 하는 상황이 발생한다.

2. GameController 클래스를 재활용하기 어려워진다.

GameController 클래스를 다른 상황에서 재사용하려고 할 때,
Game 클래스를 함께 가져와야하기 때문에 재활용이 어려워지는 문제가 발생한다.


이게 왜 결합도가 높은 거야?

다른 모듈에 대해 자세히 알수록 결합도가 높다고 할 수 있는데,
지금 GameControllerGame 객체 생성 방법까지 알고있는 상태 = 결합도가 높다고 할 수 있다!

결합도가 높다
= 다른 모듈에 대해 많이 안다
= 다른 모듈에 대해 많이 관여하고 있다
= 변경 사항을 수용하기 어렵다


참고로, 응집도는 뭐지?

클래스 내의 메소드가 서로 비슷한 작업을 수행하는지, 메소드들이 서로 관련이 깊은지를 말하는 개념이다.

응집도가 높다
= 클래스 내의 메소드가 서로 관련되고 비슷한 작업을 수행한다.
= 코드의 가독성과 유지 보수 용이성이 높아진다.


즉, 결합도가 낮고 응집도가 높을 수록

= 코드의 재사용성⬆️, 유지보수 용이성 ⬆️
= 객체지향적인 설계!


🔎 의존성 주입이란?

자 그러면 이런 객체간의 의존성을 줄이는 방법은 무엇일까?
그게 바로 의존성 주입이다!

이렇게 GameController의 외부에서 객체를 생성하고 가져와(주입) 사용하는 방법을 말한다!

의존성 주입의 장점

1. 단위 테스트가 쉬워진다.
: 내부에서 생성하는 객체는 테스트를 위해 특정 값을 주입해줄 수 없기 때문에!

2. 코드의 재활용성이 높아진다.
: GameController 클래스를 다른 상황에서 재사용하려고 할 때, Game을 의존하지 않기 때문에 재사용이 쉬워진다.

3. 객체간 의존성, 결합도가 낮아진다.

🔎 의존성 주입의 방법 4가지

그런데 의존성을 주입하는 방법은 사실 4가지이다.

생성자 주입
수정자(setter) 주입
필드 주입
일반 메서드 주입

위의 방법은 그 중 하나인 생성자 주입 방식이었던 것이다!

단, 의존성 주입의 4가지 방법 등에 있어서 스프링에 관련된 결과가 주가 되어 나오는 것으로 보아
이 다음부터 설명하는 내용에 대해서는 순수 자바 프로그래밍과 완벽히 일치하지 않는 부분이 있을 수 있다!

1. 생성자 주입 (👍가장 권장!)

public class GameController {

    private Game baseballGame;

    public GameController(Game baseballGame) {
        this.baseballGame = baseballGame;
    }
}   

장점

  1. 객체가 항상 일관된 상태를 유지한다.
    : 객체가 생성될 때 필요한 모든 의존성이 생성자를 통해 주입되기 때문에,
    : 객체가 생성된 이후에는 의존성이 변경되지 않는다.
  2. 의존성 주입을 명시적으로 나타내므로 코드의 가독성이 높아진다.
  3. 단위 테스트에 용이하다.
    : 필요한 의존성을 생성해서 주입해주면 되기 때문에!

단점

수정자(setter) 주입과 달리 객체 생성 시 의존성을 주입하므로 선택적으로 의존성을 주입할 수는 없다.

2. 수정자(setter) 주입

public class GameController {

    private Game baseballGame;

    public void setGame(Game baseballGame) {
        this.baseballGame = baseballGame;
    }
}   

장점

선택적으로 의존성을 주입할 수 있다.

단점

생성자 주입과 달리 해당 메소드가 실행되어야 의존성이 주입되기 때문에 객체의 일관성을 보장하기 어렵다.
⇒ 의존성이 누락되어 NullPointerException과 같은 예외가 발생할 수 있다.

3. 필드 주입

public class GameController {

    private Game baseballGame = new Game();

	private void start() {
        playGame(baseballGame);
    } 
}   

장점

코드가 간결하다.

단점

  1. GameController 생성자 호출 후에 Game 객체에 의존성이 주입되기 때문에 객체 상태가 변경될 수 있다.
  2. 테스트 하기가 어렵다.
    : 객체 내부에서 의존성이 주입되기 때문에
  3. 의존성 주입이 명시적이지 않고 숨어있어 코드의 가독성이 떨어진다.

잠깐, 필드 주입은 의존성 주입의 한 방법이지만 의존성 주입의 원칙을 따르지 않는다?
이게 무슨 홍철없는 홍철팀 같은 말인가...! 일단 내가 이해한 바로는,
1. 필드 주입은 의존성 주입의 한 형태이지만
2. 위의 경우 Game 객체가 GameController 내부에서 필드를 통해 직접 초기화 된다.
3. 즉, 외부에서 의존성을 주입한 것이 아니므로
3. 의존성 주입의 원칙을 따르는 방식이라고 보기 어렵다.

4. 일반 메서드 주입

public class GameController {

    private Game baseballGame;

	private void start(Game baseballGame) {
        this.baseballGame = baseballGame;
    } 
}   

장점

객체 생성 후에 의존성을 주입해야하는 경우 사용할 수 있다.

단점

필드 주입과 마찬가지로 불변성과 객체 일관성을 위반할 수 있게된다.


🔎 따라서, 생성자 주입 권장!

  1. 객체가 항상 일관된 상태를 유지한다.
  2. 의존성 주입을 명시적으로 나타내므로 코드의 가독성이 높아진다.
  3. 단위 테스트에 용이하다.

생성자 주입? 생성자 초기화?

비슷한 용어로 사용되고 있고, 초점의 차이가 있는 것 같다.

생성자 주입
객체가 생성될 때 필요한 의존성을 생성자를 통해 주입하는 방식
생성자 초기화
객체가 생성될 때 필요한 값 또는 상태를 설정하거나 초기화하는 것


그런데 이 생성자 주입으로 의존성 주입을 할 때,
Game 인터페이스와 이를 구현한 BaseballGame 객체가 있다면 둘 중 뭐를 주입하는 게 좋을까?

지금까지의 코드에서 변수명은 baseballGame인데 객체명이 Game 것을 발견한 분도 있을 것이다.
그 이유가 바로 DIP, 즉, 의존 역전 원칙에 있다!


🔎 DIP : 의존 역전 원칙

: Dependency Inversion Principle

고수준 모듈이 저수준 모듈에 직접 의존하는 것을 피해야 한다는 원칙이다.

추상화에 의존해야지, 구체화에 의존하면 안 된다 - 출처: 위키백과

고수준 모듈? : 변경이 없는 추상화된 클래스 (또는 인터페이스)
저수준 모듈? : 위의 추상화된 클래스나 인터페이스를 상속받은 구현체 클래스, 변하기 쉽다.

코드로 설명해보자면

public class BaseballGame implements Game {

    public BaseballGame init() {
        return new BaseballGame();
    }
}
------
public interface Game {

    BaseballGame init(Balls answerBalls);
}

이렇게 Game이라는 인터페이스(고수준 모듈)와 BaseballGame이라는 구현 클래스(저수준 모듈)이 있을 때,

public class GameController {

    private Game baseballGame;

    public GameController(Game baseballGame) {
        this.baseballGame = baseballGame;
    }
}   

GameController는 의존성을 주입할 때,

변하기 쉬운 저수준 모듈인 BaseballGame에 의존하지 말고
고수준 모듈인 인터페이스 Game에 의존해야한다는 의미이다!


🔎 한 줄 정리

의존성 주입
필요한 객체를 객체 내에서 직접 생성하는 것이 아니라 외부에서 객체를 생성해 받아서 사용하는 것
생성자 주입
객체가 생성될 때 생성자를 통해 의존성을 주입하는 방법

3개의 댓글

comment-user-thumbnail
2023년 10월 30일

리뷰가 도움이 되어서 정말 행복하네요!
앞으로 더 공부해서 저도 현지님의 블로그 처럼 지식을 더 많이 공유할 수 있는 사람이 되어야 겠다는 동기부여 얻고 갑니다!! 🥰🥰🥰

1개의 답글

오! 헷갈렸던 내용인데 너무 잘 정리해주셔서 이해가 단번에 되네요!! 좋은 글 감사합니다!

답글 달기