다양한 레거시 코드들을 다루며, 유지보수와 새로운 기능들을 붙히고 있는 가운데, 가장 많이 부딫히고 있는 큰 문제점 중 하나는 의존성(dependency)
문제이다.
우선 해당 코드들은 5년전 활발하게 개발되고, 3년전까지만 유지보수가 꽤나 일어 난 것을 커밋을 통해 확인한 바 있다...
의존성 문제로 일어나는 곳이 한두곳이 아니었다. 객체를 활발하게 다루는 일종의 컨트롤러 서버(현 서비스는 Back과 Front의 객체를 다루는 Controller 역할을 중간 미들웨어 역할의 서버가 컨트롤러처럼 동작하고 있다) 내에서의 메소드간의 의존성이 깊고, 이 의존성들이 일종의 콜백 지옥을 이루고 있어 그 구조를 제작자 없이 파악하기 너무 힘들었다.
또한, 현재 담당하고있는 Electron으로 제작된 데스크탑 어플리케이션의 경우, 해당 컨트롤러 서버가 C와 C++로 제작되었고, 많은 모듈들이 갖은 dependency
가 arm 의 M1과 함께 꼬여버렸다. 즉, 낮은 node 버전과, C++ 빌드를 위한 모듈 (node-gyp 등), 추가 된 빌드 파일, 다양한 node module들이 얽히고 섥혀 버렸다.
해당 문제를 해결하기 위해, 일정 시간을 공수하여 들이고싶지만, 급급한 이슈들과 신 개발이 이루어지고 있어 어려움을 갖고있다...
각설하고, 언젠가는 해결해야할 이러한 의존성 문제를 정확한 개념과 사용되는 패턴들, 그 외의 방향성에 대해 정리하고자 한다.
사실, Front 개발자라면 혹은 이를 공부하고 있는 사람이라면, 의존성 문제에 대해서 항상 자주보던 부분이 있다.
npm i --legacy-peer-deps
or
npm i --force
해당 키워드는 node module
을 설치하는 과정에서, 의존성 문제로 특정 모듈들이 설치되지 않을 때 많이 사용하는 키워드이다.
본인은 무지성으로 큰 문제가 없다면 둘 중 하나의 키워드를 이용해 강제적으로 설치하기도 하고, 이가 이루어지지 않으면
npm cache clear --force
캐시를 지워 다시 시도하기도 했다.
https://github.blog/2021-02-02-npm-7-is-now-generally-available/
해당 위치에 npm 7
과 함께 peer dependency
에 관한 내용이 있으니, 한번 읽어보는 것도 추천한다.
이렇게 우리를 괴롭히는 의존성(dependency)
란 무엇일까?
의존성(Dependency)
Dependency is a broad software engineering term used to refer when a piece of software relies on another one. Simply put, if Program A requires Program B to be able to run, Program A is dependent on Program B. This makes Program B a dependency of Program A. - https://coderslegacy.com/what-are-dependencies-in-programming/
의존성
은 소프트웨어 공학에서 한 소프트웨어가 다른 소프트웨어에 기대고 있는 것이다.
즉, A가 돌아가기 위해선 B가 돌아가야하고, 이를 A는 B에 의존한다 라고 표현한다.
객체를 다루는 프로그래밍에서는, 해당 객체나 클래스의 변수나 메소드를 참조할 수도 있고, module
을 import 하는 과정에서도 그 특정 module
에 대한 의존성이 생긴다.
의존성이 위험한 이유
의존성은 코드와 worse case scenario에 대한 유연성을 떨어뜨린다. 의존하고 있는 특정 프로그램이 변화할 때, 해당 변경이 전파될 수 있고, 참조하고 있는 다른 프로그램에서의 사이드 이팩트가 쉽게 일어날 수 있다.
의존성을 사용하는 이유?
그럼 결국 의존성이 없게끔 만들면 되는 것 아닌가? 프로그래밍에는 유명한 말이 있다.
"Don't reinvent the wheel!"
이미 나와있는 서비스/라이브러리가 존재한다면, 다시 만들지마라!
해당 라이브러리에 대한 테스트나 사용이 빈번하고, 주기적으로 업데이트를 하는 라이브러리의 경우, 개발자의 입장에서 해당 로직을 스스로 다시 만든다는 것은, 1초의 개발 시간을, 1000000000......초로 늘리는 것 과 같다...
정말 어떻게 보면 쓸모 없는 행동을 하고 있다는 것이다.
모순
그 당시의 편안함을 위해, 의존성을 무릎쓰고도 서비스/라이브러리를 도입하는 결정이나, 혹은 그것이 두려워 자체적인 코드를 구성하는 것에 대해, 모순이 일어나는 시점이다.
하지만 깔끔하게 생각해보면, 영원한 것은 없다.
당장 나만해도 현재 다니고 있는 회사에 평생 다닐 것이라고 절대 생각하지 않는다.
그렇다면, 후세에 해당 코드를 다룰 후임들을 위해, 조금 더 심플하고, 의존성이 적지만 사용하더라도 유지보수가 용이하고 Linear
하도록 만드는 것이 숙제이지 않을까?
결국 잘고르는게 답...?
사실 특정 의존성을 회피하는것은 불가능하다. 그렇다면 어떤 요소들을 고려해야할까?
Portability
가장 피해야할 것으로, 특정한 서비스나 플랫폼에서만 작동하는 의존성이다.
특정한 서비스나 플랫폼을 위해 제작하는 것이 아니라면, 여러 테스트와 확인 과정을 통해 선택해야한다.
Version control
어떠한 버전을 사용할지 선택하는 과정은 조심히 일어나야 한다.
신 버전들은 새로운 기능들을 동반하고, 클라이언트가 해당 버전보다 더 오래된 버전을 사용 할 수도 있다. 개발 과정과 함께 업데이트 주기를 고려해야한다.
Regular Updates
지속적인 개발이 일어나지 않는다는 것은, 보안적인 이슈임에 동시에, 해당 모듈이 추후에는 버려질 수도 있다는 것이다.
일년 이상 개발되지 않은 버전이라면, 과감하게 사용하지 말아야 한다.
Adoption rate and usage
프로그램을 위한 의존성을 고려할 때, 다양한 것들을 찾을 수 있을 것이다.
그중에서도 다양하고, 많이 사용된 의존성을 선택하는 것이 좋다.
이는 믿을만하고 사용할만하다고 시사하는 것 뿐만 아니라, 해당 의존성에 대한 커뮤니티가 생성되기 쉽고, 온라인 상으로 다양한 의견이 오고가며, 질의응답을 받기 쉽다는 것을 알 수 있다.
Compatibility
가장 흔히 망각하는 요소 중 하나로써, 해당 의존성이 호환할 수 있는 환경인지를 확인하는 과정은 필연적이다.
흔히 일어나는 일중 하나로, 꼭 사용 전 확인 과정은 필요하다.
의존성 주입(Dependency Injection) in Javascript
하나의 패턴으로써, A가 B객체에 의존한다면, A에서 B객체를 생성해주는 것이 아닌, A의 파라미터로 B의 클래스를 직접 받게 되면 된다.
의존성 주입(Dependency Injection)
이란 의존하고 있는 B를 내부가 아닌 외부에서 결정하고 주입하는 것이다.
큰 차이점으로는, 해당 모듈이 바뀌었을 때를 생각 할 수 있다. B가 대대적으로 바뀌게 되었을 때, A를 수정해야하는 기존의 경우와 다르게, 의존성 주입(Dependency Injection)
을 적용했을 경우, 외부의 B만 수정하여, 올바르게 작동한다면 쉽게 고칠 수 있다.
아래의 코드를 보며 이해해보자.
//users-service.js
const User = require('./User');
const UsersRepository = require('./users-repository');
async function getUsers() {
return UsersRepository.findAll();
}
async function addUser(userData) {
const user = new User(userData);
return UsersRepository.addUser(user);
}
module.exports = {
getUsers,
addUser
}
const User = require('./User');
function UsersService(usersRepository) { // 매개 변수로 넘긴다.
async function getUsers() {
return usersRepository.findAll();
}
async function addUser(userData) {
const user = new User(userData);
return usersRepository.addUser(user);
}
return {
getUsers,
addUser
};
}
module.exports = UsersService
매개변수로 받게 된 B(usersRepository)로 인해, B가 올바르게 작동하지 않아도, 매개변수만 올바르면 A는 잘 작동한다.
즉, A의 코드는 수정하지 않아도 되는 것 이다. 정말 간단한 생각이지만, 특히 Javascript에서는 이러한 패턴들이 고려되지 않고 있다.
Import 키워드와 함께 사용되어 쉽게 모듈을 사용할 수 있기 때문이다. 하지만 이러한 패턴의 미적용은 결국 추후에 코드 내부의 사용된 n개의 변수에 대해, 모두 바꿔줘야한다는 불편함을 야기한다.
IoC 컨테이너(Inversion of Control Container)
IoC란 제어권의 역전으로, 객체의 생성, 생명주기까지 모든 객체에 대한 제어권이 바뀌었다는 것을 의미한다.
DI를 위해, 다양한 의존성들을 모두 미리 세팅한다는 귀찮음이 존재한다.
const UsersRepository = require('./users-repository');
const Mailer = require('./mailer');
const Logger = require('./logger');
const UsersService = require('./users-service');
const InMemoryDataSource = require('./users-repository/data-source/in-memory');
const logger = new Logger({
level: process.env || 'dev'
});
const dataSource = new InMemoryDataSource();
const mailer = new Mailer({
templates: '/emails',
logger
});
const usersRepository = new UsersRepository({
logger,
dataSource
});
const usersService = new UsersService({
usersRepository,
mailer,
logger
});
module.exports = {
usersService
}
이러한 귀찮음을 해결 해 줄 라이브러리를 활용하면 훨씬 쉽게 구현할 수 있다.
const UsersRepository = require('./users-repository');
const Mailer = require('./mailer');
const Logger = require('./logger');
const UsersService = require('./users-service');
const InMemoryDataSource = require('./users-repository/data-source/in-memory');
const { createContainer, asClass } = require('awilix');
const createAppContainer = async () => {
const container = createContainer();
container.register({
logger: asClass(Logger).inject(
() => ({ level: process.env || 'dev'})
),
dataSource: asClass(InMemoryDataSource),
mailer: asClass(Mailer).inject(() => ({ templates: '/emails'})),
usersRepository: asClass(UsersRepository),
usersService: asClass(UsersService)
});
return container
);
(async () => {
const container = await createAppContaier();
const usersService = container.resolve('usersService');
})()
'Software Engineering' 카테고리의 다른 글
[OAuth 2.0] Microsoft ID 플랫폼과 함께 OAuth 2.0 복기하기 (0) | 2023.05.27 |
---|---|
[Web App] Frontend Developer로 Web App 개발하기 (0) | 2023.01.14 |
[Javascript] Class vs Factory Function (2) | 2022.09.24 |
Git 브랜치 전략, Git-flow (0) | 2022.09.03 |
성능을 위한 프론트엔드 설계와 코드 - 1 (0) | 2022.08.28 |