안녕하세요,
오늘은 사이드 프로젝트로 진행한 IMDB 클론에서 api 로직을 도메인으로 분리해 처리하려고 합니다.. 사실 DDD 찍먹해보기 느낌이랄까요?
post, put api 없이 get만 사용하는 사이드프로젝트다 보니 우선은 get만 분리해서 확인해보려고 합니다. 기존에 api fetch 방식과 ddd 분리 방식을 비교해보며 좀 더 깔끔한 코드가 어떤건지, 비즈니스 로직을 숨기는건 어떤 장점이 있을지에 대해 알아보려고 합니다.
기존의 API Fetch
검색화면에서 검색 내용을 토대로 영화 리스트를 받아오는 api를 만들어보았습니다.
const getSearchMovie = async ({ params }: Props): Promise<Movie[]> => { const res = await fetch( `${BASE_URL}search/movie?api_key=${process.env.API_KEY}&query=${params.searchTerm}&language=ko-KR&page=1&include_adult=false` ); if (!res.ok) { throw new Error("Error fetching data"); } const data = await res.json(); return data.results; };
간단하게 fetch를 통해 받아오고, ok로 상태체크, data.results를 return 해 줍니다. 사실은 뭐 간단 조회기도 하고해서 복잡한 로직은 없지만.. 우리는 찍먹이니까 apiService에서 data를 fetch, service에서 로직을 관리 하는 구조로 한번 변경해보도록 하겠습니다.
Domain
export class Video { id: number; backdrop_path?: string | null; poster_path?: string | null; overview: string; title?: string; name?: string; release_date?: string; first_air_date?: string; vote_count: string; constructor({ id, title, name, poster_path, backdrop_path, release_date, first_air_date, overview, vote_count }: Video) { this.id = id; this.title = title; this.name = name; this.poster_path = poster_path; this.release_date = release_date; this.first_air_date = first_air_date; this.backdrop_path = backdrop_path; this.overview = overview; this.vote_count = vote_count; } }
Video domain을 하나 만들었습니다. video는 id와 title 등, 여러가지의 값을 갖고 있습니다. 원래는 여기에 isCheck 같은거로 일치를 체크하는 로직이 들어갈 수도 있고, 기타 로직이 추가될 순 있지만 우린 찍먹이니까..
domain을 하나 만들었으면 이제 실질적인 API Fetch 로직을 수행하는 APIService 를 만들어 보겠습니다.
APIService
export class VideoAPIService { private readonly API_BASE_URL = "https://api.themoviedb.org/3/"; private readonly API_KEY = process.env.API_KEY; async searchVideos(query: string, type: string): Promise<Video[]> { const url = `${this.API_BASE_URL}search/${type}?api_key=${this.API_KEY}&query=${query}&language=ko-KR&page=1&include_adult=false`; const response = await fetch(url); if (!response.ok) { throw new Error("Error fetching data"); } const data = await response.json(); const videosData = data.results; return videosData.map((videoData: any) => new Video(videoData)); } }
여기서는 실질적인 API Fetch 업무만 수행하고 있습니다. 여기어 api 에러 핸들링을 진행합니다.
아까 Domain에서 설정한 Video를 타입으로 주어 searchVideos 라는 method 를 생성하였습니다.
Service
이제 실질적인 로직을 수행하는 Service를 구현해보도록 하겠습니다.
export class VideoService { constructor(private readonly videoAPIService: VideoAPIService) {} async searchVideos(query: string, type: string): Promise<Video[]> { return await this.videoAPIService.searchVideos(query, type); } }
지금의 경우 조회성 api만 사용하고 있어서 큰 로직은 없어서 매우 간단해 보입니다.
하지만 이러한 경우도 생각할 수 있을겁니다. 지금의 경우 단순한 리스트 조회이지만 detail 정보를 원할때 우리는 detail 정보를 얻기위해 여러 API 를 fetch, 필요한 데이터를 모아 전달해줄 수도 있을겁니다.
async getVideoDetails(videoId: number): Promise<VideoDetail> { const [video, videoReview, videoImages, videoKeyword, similarVideos] = await Promise.all([ this.videoAPIService.getVideoDetails(videoId), this.videoAPIService.getVideoReviews(videoId), this.videoAPIService.getVideoImages(videoId), this.videoAPIService.getVideoKeywords(videoId), this.videoAPIService.getSimilarVideos(videoId), ]); const videoDetail = { ...video, videoReview, videoImages, videoKeyword, similarVideos, }; return videoDetail; }
위처럼 말이죠,
위에 예시는 video에 detail을 전달하는 method 입니다. 이 method는 detail에 해당하는 details, reviews, images, keywords, similarVideos를 조회해 뿌려주게 됩니다.
이것도 복잡한 구조는 아니지만 지금 들 수 있는 간단한 예시라고 생각해서.. 그럼 이제 이렇게 전달해주는 service를 view에서 어떻게 사용할 수 있을까요?
Service 사용하기
const getSearchMovie = async ({ params, searchParams }: Props): Promise<Video[]> => { const service = new VideoService(new VideoAPIService()); const videos = await service.searchVideos(params.searchTerm, searchParams.type || "movie"); return videos; }; export default async function SearchPage({ params, searchParams }: Props) { const videos = await getSearchMovie({ params, searchParams }); return ( <> </> ); }
이렇게 사용할 수 있는거죠, 전에 우리가 사용하려던 details 의 경우는 내부 로직의 code가 좀 길지만, 이렇게 사용한다면 매우 간단하게 사용할 수 있는거죠!!
이렇게 내부 코드를 추상화 하면서 개발하는게 진정한 장점이 아닐까 싶습니다. 비즈니스 로직은 숨기면서 코드는 간결, 내부 로직 수정할때도 유용할 수 있죠.
하지만 꼭 DDD가 장점만 있는 건 아닙니다. DDD의 단점도 존재하죠!
DDD 단점
- 복잡성: DDD는 특히 경험이 없는 개발자에게 복잡하고 구현하기 어려울 수 있습니다. 이 접근 방식에는 비즈니스 도메인에 대한 깊은 이해와 상당한 양의 선행 설계 및 계획이 필요합니다. 이로 인해 개발 프로세스에 더 많은 시간과 비용이 소요될 수 있습니다.
- 가파른 학습 곡선: DDD는 소프트웨어 개발에 대한 생각의 패러다임 전환을 요구하며 이는 기존 소프트웨어 개발 접근 방식에 익숙한 개발자에게 어려울 수 있습니다. 개발자는 비즈니스 도메인을 이해하고 도메인 전문가와 긴밀히 협력해야 하며 이는 시간이 많이 걸리고 어려울 수 있습니다.
- 과잉 엔지니어링: DDD는 개발자가 유지 관리 및 확장이 어려운 지나치게 복잡한 솔루션을 만드는 과잉 엔지니어링으로 이어질 수 있습니다. 이는 개발자가 도메인의 세부 사항에 너무 몰두하여 소프트웨어의 전반적인 목표를 간과하는 경우에 특히 그렇습니다.
- 코드베이스의 복잡성 증가: DDD는 코드베이스의 복잡성 증가로 이어질 수 있습니다. 이로 인해 시간이 지남에 따라 코드를 이해하고 유지 관리하기가 어려울 수 있습니다. 또한 도메인 모델이 자주 변경되면 코드베이스를 자주 변경해야 하므로 시간과 비용이 많이 들 수 있습니다.
- 테스트: DDD는 특히 도메인 모델이 복잡한 경우 테스트를 더 어렵게 만들 수 있습니다. 테스트에는 비즈니스 도메인에 대한 깊은 이해가 필요하며 시간과 비용이 많이 소요될 수 있습니다.
결론
분명 DDD는 좋은 아키텍처라고 생각하고, 저도 좋아합니다. 비즈니스 로직을 숨기는 개발방법도 개인적으로 너무 좋아합니다.
사실 단점이라고 적은것도 내가 제대로 이해하고만 있다면 유지보수, 신규기능 추가 등에 큰 제약이 없는것도 사실이죠..
매력적입니다.. 좀 더 노력해서 꼭 전문가다운 개발을 할 수 있으면 좋을 거 같습니다!