Controller, Service, Converter의 역할 분리

0

client → request

client가 보낸 body에 담긴 데이터가 server로 전달된다.

request → controller

body에 담긴 데이터는 해당 요청에 맞게 requestDTO 객체로 씌워진다.

그 객체는 validation 검사를 통해 특정 데이터가 존재하는지와 타입이 일치하는지를 검사한 다음,

controller → service

해당 서비스에 맞는 객체로 씌워져 서비스로 보내진다.

service → database

서비스에선 schema정의한 데이터 타입을 통해 DB로 보내진다음 조회하여 가져오고

database → service

DB에서 가져온 데이터는 converter를 통해 우리가 사용할 데이터들만 추려서 service로 보내진다.

converter로 씌우지 않는다면 _id, createdAt, deletedAt 등 front측이 필요하지 않는 필드도 함께 보내질 것이다. 이를 통해 front에게 필요한 정보만 converter를 통해 걸러낼 수 있다.

또한 데이터베이스에 저장되어있는 날짜같은 필드를 알아보기 쉽게 converter를 통해 바꾼다거나, 특정 데이터에 대한 유효성을 검사할 수도 있다.

service → controller → client

그 데이터는 controller를 통해 responseDTO객체로 씌워 client에게 최종적으로 응답한다.

만약 특정 유저에 대한 정보를 가져오고, 그 유저의 팔로워들도 함께 가져오는 api가 있다 하자.

이를 front에게 다시 응답을 보내려면 두 데이터를 한 객체에 합쳐서 보내야 한다.

이렇게 여러개의 데이터를 묶고, front에게 해당 응답의 타입을 알려주기 위해 responseDto를 사용한다.

역할의 분리

Controller, service, converter, validator는 각각의 역할을 수행하며 서로의 영역을 침범하면 안된다.

책임을 분리하여 서로의 일에는 전혀 개입하지 않고 받아온 데이터와 타입을 바탕으로 자신의 일만 처리할 수 있도록하는 것이다.

이는 한 기능의 변경이 다른 곳까지 미치는 영향을 최소 하고, 이는 기능 추가 및 변경에 용이하도록 만들어 준다.

Controller와 Service의 역할분리

router.get('/stores/:storeUUID', async (req, res, next) => {
	try {
		const store = await StoreModel.findStoreByUUID(storeUUID);

		res.json({ data: store });
	} catch (error) {
		next(error);
	}
});

이렇게 controller에서 db를 접근하게 하면 안된다.
Controller는 클라이언트 요청을 처리하고 응답을 반환하는 역할을 수행해야 하며, Service는 비즈니스 로직을 처리하는 역할을 수행해야 한다.

router.get('/stores/:storeUUID', async (req, res, next) => {
	try {
		const { storeUUID } = req.params;

		const storeData = await storeService.getStore(req.userUUID, storeUUID);

		const store = new GetStoreDto(storeData);

		res.json({ data: store });
	} catch (error) {
		next(error);
	}
});

이렇게 코드를 바꾸어 준다면, 이 api는 특정 store의 정보를 가져오는 것이라고 바로 확인할 수 있고, controller는 사용자의 요청을 처리하고 응답을 반환하는 역할만을 담당하게 되고, Service는 비즈니스 로직을 처리하는 역할만 수행하게 된다.

이를 분리함으로써 코드의 응집도를 높이고, 유지보수와 테스트 용이성을 개선할 수 있다.

또한 service함수로 인해 코드의 재사용성을 높이고, 확장성을 향상시킬 수 있다.

마지막으로 가독성과 유지보수성을 높여 다른 개발자가 코드를 이해하고 수정하기 쉬워진다.


Converter의 역할 분리

async getStore(storeUUID: string) {

		const store = await StoreModel.findStoreByUUID(storeUUID);

		let storeData = {};
		
		storeData.name = store.name;
		storeData.location = store.location;

		return storeData;
}

위와 같은 코드도 service에서 converter의 역할을 대신 수행한 것이다. 이렇게 한다면 converter를 전혀 사용할 이유가 없다.

이러한 코드는 나중에 front가 해당 데이터에 대한 특정한 다른 정보를 요청하거나 service함수에서 데이터의 다른 필드가 필요해졌을 때 수정하기 까다로워 진다. 기능 추가 및 변경에 용이하지 않는 것이다.

class ModelConverter {
	static storeConverter(store: StoreDAO): Store {
		return {
			name: user.name,
			location: user.location
		};
	}
}
async getStore(storeUUID: string) {

  const store = await StoreModel.findStoreByUUID(storeUUID);

  const storeData = ModelConverter.storeConverter(store);

  return storeData;
}

이렇게 converter를 이용하여 서비스와 front에서 사용할 데이터만 mapping해주어야 한다.

이를 통해 데이터의 일관성을 유지하고 서비스에서 필요로 하는 데이터를 정확하게 활용할 수 있다.

또한 front측에서 다른 정보를 요청하거나 service에서 데이터에 대한 다른 필드가 필요하게 되었을때 converter만 수정해주면 되어서 기능 추가 및 변경에 용이해진다.

profile
https://www.youtube.com/watch?v=__9qLP846JE

0개의 댓글