2024 페스타고 개발일지(겸 회고) - 3

Glen·2024년 3월 14일
0

개발일지

목록 보기
5/8
post-thumbnail

주저리

요즘 블로그 글 작성이 잘 되지 않는 것 같다. 😂

시간이 많아지니, 시간을 너무 쉽게 사용하는 것 같기도 하고...

마지막 개발 일지를 작성한 지 약 두 달의 시간이 흘렀는데, 그 기간동안 무엇을 했는지 정리를 하려한다.

기획

페스타고의 원래 기획은 대학 축제의 줄서기를 온라인 티켓팅으로 전환하여 학교와 학생이 느끼는 불편을 해소해 주는 기능을 목표로 했다.

축제 조회 기능도 염두에 두고 있었지만, 아무래도 핵심 기능이 티켓 예매이다 보니 그렇게 신경을 쓰지 못했다.

거의 90%의 시간과 노력을 모두 티켓팅에만 집중하지 않았나 싶다.

지금 생각하면 50% 정도로 하고, 조회에도 신경을 썼어야 했는데.. 😂

그때의 잘못된 선택으로 인해 지금 소 잃고 열심히 외양간을 고치는 중이다.

티켓을 예매하려면 우선 학교와 계약을 해야 했는데, 학교와 계약을 하려면 우리 프로젝트에 신뢰도가 있어야 했다.

하지만 신생 프로젝트에 조회 기능도 빈약한 프로젝트가 그럴 리가 없었다.

대학 측에 이메일을 보내봤지만, 회신이 온 한 학교를 제외하고 연락이 없었다. 😂😂😂

회신의 내용도 축제 기획이 이미 끝나서, 힘들 것 같다는 내용이었고...

그때 되어서야 첫 단추가 잘못 끼워졌다는 게 느껴졌다.

하지만 이미 프로젝트 진행은 절반 이상이 되어버렸고, 여기서 티켓팅 기능을 엎어버리기에 잃어야 할 게 너무 많았다.

(iOS 어플이 없는데, 티켓팅 서비스를 출시한다는 게 말이 안 되긴 했다 😂)

그러다 우테코 기간이 끝나고도 프로젝트를 계속 진행하기로 했고, 프로젝트의 신뢰를 확보하기 이전 iOS 개발자를 구하는 게 급선무였다.

다행이 기적적으로 iOS 개발자 한 분을 모실 수 있었고, 호박이 덩굴째 굴러온 것 마냥 디자이너 두 분도 모실 수 있었다.

새로운 iOS 개발자, 디자이너분과 함께 기획을 새로 한 결과는 디자인 전반을 갈아엎고, 신뢰도를 위해 티켓 예매 기능보다 조회 기능을 탄탄하게 만드는 것이었다.

(디자이너분이 만드신 시안을 보고 느낀 점은 조회 기능을 우리가 만들었으면 조회 기능까지 갈아엎어야 했을 것이다 😂)

지역 축제나 티켓팅이 있는 축제를 조회하는 서비스는 많지만, 대학 축제를 전문으로 조회하는 서비스는 많지 않았다.

있다고 해도 관리가 되지 않고, 디자인과 기능이 그렇게 좋아 보이지도 않았다.

또한 팀원이 주변에 물어본 결과 대학 축제 모아보기에 관한 수요가 확실하게 있었다.

따라서 조회 기능만 있는 서비스라고 할지라도 충분히 경쟁력 있는 서비스가 될 수 있다고 판단했다.

단순한 조회 기능이라 하더라도 어떤 조회 기능을 제공하냐에 따라 서비스의 경쟁력이 달라진다고 생각한다.

따라서 충분히 경쟁력 있는 서비스가 되기 위해 팀원들과 여러 아이디어를 공유했고, 대학 축제라는 도메인 한정으로 아이디어가 나왔다.

나는 시끄러운 분위기를 싫어해 대학 축제를 잘 가지 않아 도메인 지식이 부족하지만, 대학 축제를 가려는 이유는 알고 있다.

학생들이 대학 축제를 가는 것은 대학이 보고 싶은 게 아니라, 축제에 오는 가수를 보기 위해서이다.

즉, 축제를 조회할 때 어떤 학교에 축제가 있는지도 중요하지만, 축제에 어떤 가수가 오는지가 더 중요하다.

따라서 검색의 경우, 사용자는 축제의 이름을 통한 검색보다 가수 또는 대학을 목적으로 검색을 이용할 것이다.

그리고 관심 있는 가수를 북마크에 등록하여, 북마크에 등록된 가수의 축제가 개최되면 알림을 보내주는 기능도 중요하다.

따라서 해당 기획을 모두 구현한다면 조회 기능만 있는 서비스라도 충분히 경쟁력이 될 수 있을 것이라 생각한다.

양방향 의존 리팩터링

프로젝트의 코드는 우테코 크루들의 집단 지성과 코드 리뷰를 통해 퀄리티 자체는 나쁘지 않다고 생각한다.

하지만, 그 당시 생각하지 않았던 양방향 의존성 문제를 뒤늦게 깨달아 버려서 프로젝트 코드 전반을 수정해야 했다.

개발 단계 또는 서비스 운영 중에 이 문제를 수정하려면, 신규 개발을 멈추거나 충돌로 인해 여러 문제가 발생할 가능성이 있었지만, 지금은 기획 단계라 기능 개발을 할 작업이 없어서 해당 문제를 수정했다.

패키지와 도메인의 의존 방향은 공연 -> 축제 -> 학교 와 같이 단방향으로 흐르게 했다.

하지만 단방향으로 의존을 흐르게 하면, 비즈니스 로직을 구현할 수 없는 문제가 생긴다.

예를 들어, 가수를 DB에서 삭제해야 하는 일이 생길 때, 무작정 가수를 지우는 것은 문제가 될 수 있다.

외래키 제약 조건이 걸려있다면, DB에 삭제가 되지 않을 거지만, DB의 제약 조건만 믿고 삭제하는 것은 비즈니스 로직이 특정 서비스 구현체에 대한 의존을 갖게 한다.

따라서 삭제하려는 가수가 다른 엔티티에 참조가 되는지 검증 로직을 작성할 필요가 있는데, 해당 검증 로직을 구현해야 하므로 양방향 의존이 필요하다.

이것을 해결하기 위해 스프링의 의존성 주입을 사용한 인터페이스를 활용하거나 이벤트를 사용하면 코드에서 양방향 의존을 제거하면서, 검증 로직을 구현할 수 있다.

구현에서 인터페이스 또는 이벤트 둘 중 하나를 선택해야 했는데, 검증과 같이 동기적으로 반드시 처리되어야 할 로직은 인터페이스를 사용했고, 동기적으로 처리될 필요가 없는 부가 로직은 이벤트를 사용하였다.

하지만 이 경우 테스트에서 문제가 생긴다.

인터페이스로 구현된 경우 스프링의 의존성 주입에 의존하기 때문에 스프링의 실행 환경이 아니라면 직접 검증 클래스를 주입하여 테스트해야 한다.

이벤트로 구현된 경우 단위 테스트로 이벤트의 발행은 가능해도, 이벤트의 수신이 되지 않기에 검증할 수 없다.

인터페이스의 경우 사용되지 않는 검증 로직은 삭제하여 혼란이 없도록 하고, 코드 리뷰를 통해 구현된 검증 로직의 주입이 되지 않았는지 검사하면 문제를 막을 수 있을 것이다.

이벤트의 경우, 이벤트로 인해 실행되는 로직은 오로지 부가 로직으로 판단하고 설계해야 한다.

이벤트라는 말 그대로, 이미 벌어졌던 사건에 대한 의미를 가지므로 이벤트로 수신되어 실행된 결과를 단위 테스트 레벨에서 검증할 필요는 없다고 생각한다.

만약 이벤트로 실행될 결과를 반드시 검증해야 한다는 것은 단위 테스트를 넘어 통합 테스트로 봐야 하지 않을까?

따라서 테스트에 문제가 있긴 해도, 문제가 되지 않는 상황이라고 생각한다.

양방향 의존이 있어도 코드가 실행되는 데는 문제가 없지만 도메인 하나를 수정했더니 연관 없는 도메인에 수정이 생길 것이다.

그렇게 되면 다른 팀원이 작업하고 있던 PR을 머지하려고 했더니, 충돌이 발생하고, 그 충돌을 해결하느라 시간을 쏟게 된다. 😂

기능을 확장하는 데 있어 전혀 연관 없는 수정 때문에 발목을 잡히니, 확장에 불리한 설계가 되어버리는 것이다.

간단한 CQRS 적용

기존 티켓 예매 기능만 집중할 때 프로젝트 코드에는 조회 기능이 매우 빈약했다.

커서 기반 페이징은 커녕, 오프셋 기반 페이징도 적용되어 있지 않았다.

당연히 QueryDSL 같은 라이브러리를 사용하지도 않았다.

따라서 조회 기능은 단순히 Repository.findAll()을 통해 모든 엔티티를 불러와 엔티티를 직접 DTO로 매핑하여 반환해주고 있었다.

그러다 보니 조회 기능을 구현하는데 있어, 엔티티를 DTO로 변환하는 로직이 거의 대부분이었다.

사실 조회 로직은 특별한 비즈니스 로직이 없다.

비즈니스 로직이라 하더라도 동적 쿼리 또는 페이징이 아닐까 하는데, 해당 로직은 어플리케이션 레벨에서 수행하는게 아닌, SQL 레벨에서 수행되어야 한다.

즉 쿼리 그 자체가 비즈니스 로직이 되는 것이다.

따라서 쿼리로 비즈니스 로직을 구현해야 하는것을 굳이 JPA를 사용하여 엔티티를 끌고와서 그걸 또 다시 어플리케이션 레벨에서 매핑하고 있는 꼴이었다.

마치 못을 박아야 하는데 망치로 못을 박는게 아닌, 톱으로 못을 박는 느낌적인 느낌

또한 쿼리 로직(Read)과 커맨드 로직(Create, Update, Delete)이 같은 Service 클래스에 구현되어 있었다.

readOnly 속성이 붙은 @Transcational 어노테이션도 어떤 메서드에는 붙어있고, 어떤 메서드에는 붙어 있지 않았고..

그에 따라 Service 클래스는 가독성이 낮은 문제가 있었다.

또한 프로젝트는 웹을 대상으로 하는게 아닌, 앱을 대상으로 하기 때문에 API 버저닝이 적용될 필요가 있었는데, 조회 로직에서 버저닝을 지원하기 위해 기존의 메서드는 유지된 채 새로운 메서드가 생길 필요가 있다.

즉, 조회 로직이 생기는데, CUD에 관련된 Service도 영향을 받는다.

따라서 문제를 해결하기 위해 간단한 CQRS 패턴을 적용했다.

CUD를 담당하는 CommandService@Transcational을 클래스 레벨에 적용하고, Repoistory 인터페이스를 의존한다.

R을 담당하는 QueryServicereadOnly=true 속성이 적용된 @Transcational을 클래스 레벨에 적용하고, QueryDSL Repository 구체 클래스를 의존한다.

또한 QueryService는 하나의 API와 매우 밀접하게 연관되어 있고, API 명세가 변하면 QueryDSL Repository 까지 변경이 전파된다. (API 경로가 변경되는 것이 아닌, JSON 명세 변경)

따라서 Command와 Query를 별도 Service로 분리하여, 유지보수성을 높였다.

축제 조회 시 1:n 관계의 공연과 아티스트 조회

사용자가 축제 목록을 조회할 때 단순 축제 정보 뿐 아닌, 축제에 참여하는 아티스트의 정보도 볼 수 있어야 한다.

하지만 축제에 참여하는 아티스트는 축제가 아닌, 축제와 1:N 연관 관계에 있는 공연과 N:M 관계를 가진다.

해당 쿼리를 QueryDSL으로 구현하면 다음과 같이 작성해야 한다.

select(...)
    .from(festival)
    .leftJoin(stage).on(stage.festvalId.eq(festival.id))
    .leftJoin(stageArtist).on(stageArtist.stageId.eq(stage.id))
    .leftJoin(artist).on(artist.id.eq(stageArtist.artistId))
    ...

하지만 해당 쿼리를 사용할 수 없는데, 이유는 다음과 같다.

축제 목록 조회에 페이징을 적용해야 하는데, 나온 결과가 Artist 수 만큼 조회되므로 한 번의 쿼리로 페이징이 불가능하다.

또한 결과의 Artist는 Stage에 속하므로 Artist를 각 Stage에 매핑을 해야하는 과정이 필요하다.

어떻게 두 번의 쿼리로 나눠 보낸 뒤, 결과를 Map 형식으로 받아와 매핑할 수 있지만 코드의 복잡도와 자주 사용되는 조회 로직 특성상 서버와 DB에 부담을 줄 게 뻔했다.

특히 사비로 운영되는 서비스다 보니, 높은 사양의 서버를 사용할 수 없어 가장 효율적인 방법을 사용해도 모자랄 판에..

팀원들과 고민 끝에 내린 결정은 Festival에 속한 각 Stage의 Artist 목록을 미리 JSON으로 저장한 뒤, 그것을 반환하는 방법을 사용했다.

select(...)
    .from(festival)
    .innerJoin(festivalQueryInfo).on(festivalQueryInfo.festivalId.eq(festival.id))

이 방법을 사용하면 정합성을 맞추는 작업이 중요하다.

정합성을 맞추는 방법은 공연에 변경이 생길 때 이벤트를 발행하여 해결했다.

축제에 참여하는 아티스트는 정확하게 말하면 축제에 참여하는 게 아닌, 공연에 참여하는 것이다.

따라서 축제의 변경이 아닌 공연에 변경이 있을 때 JSON 값을 업데이트 하면 된다.

다만 여기서 한 가지 문제가 생기는데, Festival 도메인에서 Artist 목록을 JSON 값으로 직렬화할 때 양방향 의존 문제가 생긴다는 것이다.

위에서 말했듯, 아티스트는 축제에 참여하지 않고, 공연에 참여한다.

하지만 축제에 참여하는 아티스트 목록을 알고 싶다면, 축제가 공연에 대해 알아야 한다.

따라서 이 방법은 축제의 식별자로 아티스트 목록을 반환하는 메서드가 있는 인터페이스를 정의하여, 해당 구현체를 Stage 도메인에 구현하여 해결하였다.

그런데 이렇게 커맨드에서 양방향 의존을 제거했어도, 쿼리에서 발생하는 양방향 의존은 어떻게 해야 할지 모르겠다.

축제를 조회할 때, 공연이 조회되어야 하는 것은 지극히 자연스러운데, 이걸 없애자고 쿼리에도 인터페이스를 사용하기엔 너무 과한 게 아닌가..

그래서 차라리 쿼리 로직을 어드민 패키지처럼 별도의 패키지로 분류해야 하나 생각이 든다.

지금은 문제가 발생한 적이 없으니, 추후 문제가 발생하면 고려해야 할 듯 하다.

프로젝트

우테코 기간이 끝나고 나서도 프로젝트를 진행하는 팀은 있기는 하지만, 제일 열심히 하는 팀은 우리가 아닐까 😂

좋게 말하면 열심히 하는 것이고, 나쁘게 말하자면 서비스를 출시하지 못했기에 지금도 하는 것이라 말할 수 있겠다.

뭐 좋게 생각해서 열심히 한다고 해도, 열심히 하는 이유는 후자의 경우도 맞는 것 같다.

지금껏 프로젝트를 여럿 해왔지만, 실제로 유지가 되는 서비스를 해본 적이 없다.

거의 SI와 마찬가지로 개발만 하고, 그 이후인 운영 단계를 한 번도 경험해 보지 못했다.

이런 내가 서비스 기업에 취업하고 기여할 자격이 있다고 생각하지 않는다.

따라서 이 프로젝트를 운영해 보며, 여러 경험을 쌓을 수 있는 기회를 얻고 싶다.

또한 프로젝트 코드의 품질을 조금 과할 정도로 신경을 쓰는 편인데, 이는 책임감을 지키기 위함도 있다.

해당 프로젝트 레포는 우테코 Organization에 있는데, 여기에 올려진 레포들은 많은 사람들이 참조한다.

게다가 6기 또는 그 이후 기수들도 당연하게 프로젝트를 만들 때 우리가 만든 프로젝트를 참고할 것이다.

그런데 프로젝트의 코드를 지저분한 상태로 방치해두고, 제대로 돌아가지 않는 상태로 놔둔다면 나 스스로가 많이 부끄러워질 것 같다.

특히나 잘못된 코드나 구조를 방치한다면, 참조하는 다른 사람들이 그러한 구조를 참고할 텐데, 이는 내가 우테코에서 배웠던 교육 철학과 맞지 않는다 생각한다.

그게 아니더라도, 떠나는 사람이 갖춰야 할 것은 인수인계를 제대로 하는 것이라 생각한다.

하지만 제대로 돌아가지 않는 코드를 방치해두는 것은 인수인계를 제대로 하지 않은 것이라 볼 수 있다.

남들이 다 가고 싶어 한 교육 기관에서 무상으로 교육을 받은 만큼, 그만큼의 책임감을 가져야 한다.

그렇다고 내가 작성한 코드가 정답은 아니지만, 오답은 피해야 하지 않을까.


두 달간 있었던 일을 대략 정리했는데, 그때 고민했던 내용들을 미리 정리해 둘 걸 그랬다. 😂

매주 회의에서 나눈 얘기는 많고, 노션에도 회의록을 적어뒀는데 나 혼자 생각한 아이디어는 그냥 생각으로만 정리해 둔 것 같다.

지금 그때의 생각을 떠올리려고 하니, 떠오르지 않는다. 😂

생각을 메모해 두는 습관이 정말로 중요한 것 같다.

아마 개발일지 방향을 회의에서 나눈 얘기와 고민과 생각들로 적지 않을까 싶다.

profile
꾸준히 성장하고 싶은 사람

0개의 댓글