DI, IOC란
DI, IOC 단어는 스프링과 라라벨 같은 웹 프레임워크를 하다 보면 알아야 하는 개념 중 하나입니다.
해당 포스트에서는 DI, IOC 개념을 알 수 있습니다.
DI
Dependency Injection(의존성 주입)은 객체의 의존 관계를 외부에서 주입한다.
여기서 의존 관계는 아래 코드로 알 수 있습니다.
class AClass{
function test(){
echo "test";
}
}
class BClass{
private $internalAClass;
public function __construct(AClass $internalAClass){
$this->internalAClass = $internalAClass;
}
public function getATest(){
echo $this->internalAClass->test();
}
}
$BClass = new BClass(new AClass());
$BClass->getATest();
지금 BClass의 getATest를 사용하기 위해서는 AClass의 인스턴스를 받아서 생성자로 할당하는 작업이 필요합니다.
이때 BClass는 AClass에 의존관계를 가지게 됩니다.
의존성주입이란 이 작업을 외부에서 주입을 해주는 것을 의미합니다.
일반적으로는 3가지 방법이 있습니다.
- 생성자 주입 : 생성자로 주입, (위의 쓴 예제가 생성자 주입)
- 세터 주입 : Setter 메서드를 선언하여 주입
- 메서드 주입 : Setter가 아닌 메서드를 작성하여 주입
만약 위 예제에서 의존관계가 많아진다면 코드 수정의 작업 수가 많아질 것입니다.
class BClass{
private $internalAClass;
private $internalA1Class;
private $internalA2Class;
private $internalA3Class;
public function __construct(
AClass $internalAClass,
A1Class $internalA1Class,
A2Class $internalA2Class,
A3Class $internalA3Class,
){
$this->internalAClass = $internalAClass;
$this->internalA1Class = $internalA1Class;
$this->internalA2Class = $internalA2Class;
$this->internalA3Class = $internalA3Class;
}
...
}
$BClass = new BClass(
new AClass()
new A1Class()
new A2Class()
new A3Class()
);
위와 같이 사용할 경우 BClass를 사용하는 부분마다 수많은 AClass와의 의존관계를 넣어줘야 하게 됩니다.
※ 클라이언트 : 여기서는 대상 로직을 실행하는 로직을 의미
여러 웹 프레임워크에서는 이를 알아서 의존 관계 주입을 해주는데
라라벨은 컨테이너가 클래스의 생성자의 타입힌트를 읽어 인스턴스를 자동 오토와이어링해줍니다.
기존 라라벨로 개발해 봤던 분들은 Controller 클래스에서 인스턴스를 생략했던 게 기억나실 텐데
그럴 수 있던 이유가 라라벨에서 자동으로 의존성 주입(DI)을 해주기 때문입니다.
Contoller 클래스 예시
class UserController extends Controller
{
protected $users;
// 생성자에서 UserRepository를 오토와이어링
public function __construct(UserRepository $users)
{
$this->users = $users;
}
public function show($id)
{
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}
의존성 주입을 하게 되면 아래의 이점들을 얻을 수 있습니다.
- 코드 재사용을 높여서 소스 코드의 수정 없이 사용
- 의존성 주입부를 모의 객체로 바꾸기 쉽기에 테스트의 편리성을 높임
- 의존 관계 설정이 실행 시에 이루어져 모듈들 간의 결합도를 낮춰 유연한 코드 개발이 가능
- 클라이언트는 의존관계 객체 내부의 구체적인 기능을 몰라도 되기에 유지보수성을 높임
IOC
Inversion of Control(제어의 역전)은 개발자가 직접 컨트롤하는 게 아닌 프레임워크가 호출한다.
DI 설명에서 프레임워크에서 DI를 해준다 하였는데 그게 IOC의 개념입니다.
프레임워크가 객체(대부분 Singleton을 사용한 객체)의 생성부터 주입(DI), 변경, 소멸까지의 생명주기(Life Cycle)를 관리하여 개발자로 하여금 개발에 집중에만 집중할 수 있게 합니다.
IOC를 사용하면 좋은 점은 객체지향 설계의 5원칙의 OCP, DIP 지키기 용이해집니다
SOLID : [OCP, DIP]
- OCP(Open/closed principle) : 개방-폐쇄 원칙
소프트웨어는 확장은 자유로우나 수정에는 닫혀있어야 한다. - DIP(Dependency inversion principle) : 의존관계 역전 원칙
클라이언트는 구현체가 아닌 인터페이스에 의존해야 한다.
나머지는 아래 주소 참고
정의만을 보았을 때는 알 수 없습니다, 실제 코드로 이를 적용하기는 까다롭습니다.
하지만 이 점을 알고 있는 대부분의 웹 프레임워크가 이점을 해결했기에 프레임워크 사용 시 쉽게 적용 가능합니다.
라라벨에서는 아래 예시처럼 하면 OCP, DIP 원칙을 적용할 수 있습니다.
관련 예제를 만드는데
아래 순서대로 진행합니다.
- 인터페이스 생성
- 구현체 생성
- Provider 등록
- IOC 적용
인터페이스 생성
Repository Interface를 만들어줍시다, Repository라 한 이유는 Repository 패턴을 사용하기 때문입니다.
주제가 IOC, DI이므로 다음에 포스트에서 정리하겠습니다.
※ Repository 패턴 : 모델의 데이터베이스 관련 기능을 Repository로 빼고 모델은 모델끼리의 관계나 Accessor 등을 지님
Reposiotry는 쉽게 생각하면 DAO
namespace App\Repositories\Interfaces;
interface UserRepositoryInterface extends BaseRepositoryInterface
{
/**
* 유저 아이디로 데이터 찾기
* @param $id : user id
* @return mixed
*/
public function getOneById($id);
}
구현체 생성
Repository Interface를 구현한 UserRepository를 만들어줍시다.
namespace App\Repositories\Implementations;
use App\Models\User;
use App\Repositories\Interfaces\UserRepositoryInterface;
class UserRepository implements UserRepositoryInterface
{
private $model;
// User 모델로 오토와이어링
public function __construct(User $model)
{
$this->model = $model;
}
public function getOneById($id)
{
return $this->model->find($id);
}
}
Provider 생성
라라벨에서 인터페이스와 구현체를 찾을 수 있도록 바인딩해줍니다.
app\Providers\RepositoryProvider.php를 만들어서 UserRepositoryInterface::class에 UserRepository::class를 싱글톤으로 바인딩해줍니다.
※ 싱글톤 : 바인딩마다 새로운 인스턴스를 만드는 게 아닌 캐싱된 인스턴스를 사용, 주의점으로는 싱글톤은 무상태여야 함
class RepositoryProvider extends ServiceProvider
{
public function register()
{
$this->registerResponseBindings();
}
protected function registerResponseBindings()
{
$this->app->singleton(UserRepositoryInterface::class, UserRepository::class);
}
public function boot()
{
//
}
}
config\app.php의 providers에 추가합니다.
'providers' => [
...
App\Providers\RepositoryProvider::class,
],
이제 UserRepositoryInterface는 UserRepository 바인딩되었으므로 내부 컨테이너에서 오토와이어링 할 때
UserRepositoryInterface을 만나면 UserRepository로 DI 해줍니다.
IOC
UserController를 만들고
namespace App\Http\Controllers;
use App\Repositories\Interfaces\UserRepositoryInterface;
class UserController
{
private $userRepository;
// 바인딩을 통해 타입힌트로 UserRepositoryInterface를 만나면
// 컨테이너에서 UserRepository로 인스턴스화
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
public function index(){
return response()->json(
$this->userRepository->getOneById(1)
);
}
}
web.php에도 라우팅 추가
Route::get('test', [\App\Http\Controllers\UserController::class, 'index']);
이제 test로 접속했을 때 테이블이 존재하고 id가 1인 값이 있으면 JSON 데이터로 id가 1인 user 데이터를 불러옵니다.
이게 왜 IOC인지는 개발자에 경우 UserRepositoryInterface를 사용하면 프레임워크에서 자동적으로 UserRepository로
의존성을 넣어주기에 제어의 역전인 것입니다.
DI, IOC 사용 이점
SOLID 원칙을 준수
SOLID 원칙 중 가장 지키기 어려운 DIP, OCP 원칙을 준수하게 됩니다.
- OCP(Open/closed principle) : 개방-폐쇄 원칙
소프트웨어는 확장은 자유로우나 수정에는 닫혀있어야 한다.
=> Interface의 다형성을 이용해 구현 등으로 확장은 용이하고 수정의 경우 Interface를 따라야 하므로 닫혀있습니다. - DIP(Dependency inversion principle) : 의존관계 역전 원칙
클라이언트는 구현체가 아닌 인터페이스에 의존해야 한다.
=> 클라이언트 로직에서는 구현체로 변수를 만드는 게 아닌 인터페이스로 변수로 만들기 때문에 개발자는 인스턴스에 신경 쓸 필요가 없습니다.
개발했을 때
DI로 의존성 주입부를 모의 객체로 바꾸기 쉽기에 테스트의 편리성을 높이기 좋습니다
아래처럼 DI를 사용하지 않고 Repository를 바로 사용할 경우
class UserController
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function index(){
return response()->json(
$this->userRepository->getOneById(1)
);
}
}
테스트가 필요할 때 기존 UserRepository를 바로 변경하는 리스크를 가지거나
또는 테스트 클래스를 만들고 이를 UserRepository를 사용하는 클라이언트부를 호출하도록 수정하는 일이 발생합니다.
하지만 IOC를 적용한 상태라면 클라이언트 코드를 그대로 둔 채
class UserController
{
private $userRepository;
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
public function index(){
return response()->json(
$this->userRepository->getOneById(1)
);
}
}
RepositoryProvider만 수정하면 됩니다.
$this->app->singleton(UserRepositoryInterface::class, UserRepository::class);
=>
$this->app->singleton(UserRepositoryInterface::class, NewUserRepository::class);
후기
DI/IOC 패턴이 가지는 의도는 알 수 있으나 개발할 때에 있어서는 어려움이 생기기 좋은 것 같습니다.
예를 들어 신규 프로젝트를 진행하고 있는 데 중간에 신입 개발자가 들어올 경우 DI/IOC 패턴을 몰라 헤맬 수도 있고
DI, IOC 구조상 클라이언트 부가 아닌 곳에 별도로 정의되어 있기 때문에 이를 알려줄 사람이 부재일 경우 쉽지 않을 것 같습니다.
그리고 오히려 작은 프로젝트에서는 추가해야 하는 코드의 양이 많아지기에 생산성을 떨어트리지 않을까라는 생각도 들었습니다.
결론은 프로젝트를 시작하기 전에 "이 프로젝트는 SOLID를 지켜야 하는지"를 결론 내린 후 적용해야 한다 생각합니다.
감사합니다.
'PHP > Laravel' 카테고리의 다른 글
Laravel Routing이 Apache Routing 보다 우선순위가 낮을 때 (0) | 2023.06.14 |
---|---|
[Laravel9] 라라벨 DB 백업 명령어 만들기 및 자동화 with Artisan console (0) | 2022.11.01 |
[Laravel9] 라라벨 아티즌 콘솔 명령어 만들기(Artisan console) (2) | 2022.10.31 |
[Laravel9] 라라벨 테스팅 만들기 with Trait, Factory (0) | 2022.10.28 |
[Laravel9] 라라벨 이메일 보내기 with Google SMTP, Markdown (0) | 2022.10.24 |