작업 후기를 쓰고 싶은데... 먼가 혼자 주르륵 쓰기 힘들어서 친구랑 대화하는 형식으로 한명 불러오겠습니다...
루나야 도와줘~(동물의 숲에 나오는 토끼 친구입니다...ㅎ)
루나🐰 : 뱃지 서비스가 머야?
나 : 뱃지는 학생 뱃지와 랭킹 뱃지 두가지가 있어
나 : 학생 뱃지는 동아리 친구들만 사용하고, 랭킹 뱃지는 코드포스 유저 누구나 사용할 수 있게 계획했어,
아직 누구에게만 주고 다른 사람은 막는 기능은 없지만...ㅠㅠ
루나🐰 : 그래서 뱃지 서비스는 어떻게 돌아가지..?
나 : 유저가 필요한 정보를 담아서 요청하면 뱃지 정보를 전달해줘!, e.g. GET /badge?name=mark
루나🐰 : 그게 끝이야?
나 : 맞아... 특별한 권한없이 누구나..! 사용할 수 있어(학생 뱃지는 안돼구...ㅎ)
루나🐰 : 이번에 고쳤다는건 무슨 뱃지야??
나 : 랭킹 뱃지를 고쳤어!
루나🐰 : 잘 작동하고 있었는데 멀쩡한걸 뜯은거야?!
나 : 처음 랭킹 뱃지를 만들었을때는 랭킹 정보가 있는 사이트면 전부 뱃지를 제작할 수 있게 생각했거든
나 : 근데 이게 처음에 너무 복잡하게 만들어서 손을 댈수가 없더라구
나 : 뭐하나 고치려면 중국집가서 짜장면 먹다가 부족해서 군만두 시키니까 2시간 걸리는..? 일이 생기더라구(예시가 맞나?!)
루나🐰 : 어쩌다 그렇게 복잡하게 구현한거지..? 그냥 처음부터 "잘"했으면 되잖아~
나 : "처음부터 완벽"한게 정말 말도 안돼게 힘들더라구
나 : 우선 그때가 거의 2년전이거든? 자바라는 언어도 잘 몰랐고, 스프링도 처음이다보니 헤멘 일이 많았어, 전문용어로 "삽질"했다!
나 : 또 막상 구현해야하는 기능이 코드포스 랭킹 뱃지 "하나"다보니 "확장"을 고려하지 않게 돼더라구
나 : 쉽게말해 완벽하게 만들 능력도, 동기도 부족했던거지
루나🐰 : 그래서 문제는 뭐였어? 어떤걸 고치고 싶었던거야?
나 : 일단 뱃지 도메인을 분리하고 싶었어, 기존에는 인덱스라는 온갖 "도메인 비빔밥"에 함께 있었거든!
나: 심지어 컨트롤러 레이어에 뱃지를 만드는 구현 코드가 있었어!!
나 : 이건 인덱스 컨트롤러의 소스코드 히스토리거든 방향키 왼쪽을 누르면 맨 뒤에 "비빔밥"을 확인할 수 있어!(심지어 여기는 아직도😭)
루나🐰 : 와... 컨트롤러 하나가 500줄이 넘네... 코드에 늘어진 문자열 저거는 도대체 머야?
나 : 뱃지를 그려주는 코드야 우리는 뱃지 그림을 소스코드로 제어하고 있었거든
루나🐰 : 그림을 코드로 관리하다니... 미쳤어~?
나 : 도입 당시에는 단점보다 장점이 많다고 생각했어
나 : 일단 개발자이다보니 읽을만하다는 점, 그림에 있는 특정한 문자는 유저 닉네임으로 대체했어야 했거든
나 : 이런 작업이 코드로 가능하다보니 편하더라구, 진짜 그림이었더라면 그려야 했겠지..?
나 : 일종의 히스토리를 남기는것도 편했던것 같아
나 : 그림의 특정 부분을 바꾸는 경우, 코드로 관리하면 어떤 "라인"에 변경이 있는지 확인 가능했거든
나 : 진짜 그림이라면 "틀린 그림찾기"가 되었을껄..?😎
루나🐰 : 근데 뱃지 구현 너무 긴데 가독성을 헤치면서까지 그런 방식을 고집해야했어??
나 : 이게 가슴 아픈점중 하나였지, 당시에는 구현에 급급하다보니 가독성을 그냥 "포기"하게 된것 같아...
나 : 하지만..! 이번 리팩토링에서 방법을 짜냈지!!
루나🐰 : 어떤 방법을?
나 : 구현부를 파일로 추출했어, 그림을 코드로 구현한 장점은 모두 챙기면서 가독성을 크게 헤치는 단점은 가린거지!!
루나🐰 : 흠... 복잡한 내용을 한번 감싸서 읽을만하게 만들다니 단점을 설계로 극복했구나!
나 : 맞아 앞으로도 이런 방식은 자주 나오는 방법이야
나 : 트레이드 오프라는 말 많이들 하잖아 사실 나는 두마리 토끼를 잡을 수 있다고 생각하거든
나 : 그러려고 공부하는거지 하하...😆😆😆
루나🐰 : 그래서 다음으로 고친건 뭔데?
나 : 레이어 아키텍쳐를 헤치는 부분을 고쳤어, 컨트롤러에 존재하는 로직을 서비스 레이어로 하나 내리는 작업을 했지
루나🐰 : 그냥 컨트롤러에 로직이 있으면 안돼?
나 : 음.. 우선 그대로 두면 가독성이 너무 떨어지는 것같아 컨트롤러는 유저의 요청을 받는 역할이잖아
나 : 뱃지 로직까지 처리하면 역할이 커지는 문제가 생기는거지
나 : 또 우리는 스프링을 사용하고 있었는데 이 친구의 장점이 개발자에게 어느정도 "사용 패턴"을 강제한다같거든
나 : 어노테이션 붙히면서 애써 지키려는걸 정면으로 위반하는? 느낌인거지~
루나🐰 : 그럼 왜 레이어 하나만 내린거야? 어차피 그런건 도메인에 있어야하는 로직이잖아?
나 : 한번에 뒤엎는건 역시 너무 어렵더라구...🤔🤔
나 : 문제를 쪼개서 깨보려고 그렇게 접근했어🛠
루나🐰 : 서비스 레이어 리팩토링은 어떻게 진행했어?
나 : 서비스에서도 도메인으로 분리할만한 내용을 분리했어
나 : 다만 이번에는 "확장"을 고려해서 조금 기술이 들어갔지...😎😎
루나🐰 : 오... 드디어 다른 랭킹 사이트를 염두해둔 구현을..?
나 : 맞아 뱃지를 만들여면 랭킹 정보를 웹사이트에서 가져와야 하거든
나 : "외부 랭킹 사이트와의 접점에 인터페이스"를 하나 뚫었어, 외부 인프라를 한번 감싼거지
루나🐰 : 그 유명한 "헥사고날 아키텍쳐"?!?!
나 : 그치.. 어뎁터, 어플리케이션, 도메인 구조를 적용해서 더이상 직접 외부 사이트에서 요청을 받지 않아
나 : 대신에 인터페이스를 이용해서 레이팅 정보를 받아오지, 이 인터페이스 구현체는 어뎁터에 위치해
나 : 서비스 레이어에서는 각각의 웹사이트별로 어떻게 응답을 받아오는지에 대한 구현은 전혀 몰라!
package uhs.alphabet.badge.domain; // 도메인은 외부에 어떤 의존성도 가지지 않는다
public class RankedBadge {
// 구현
}
[나 : import문을 살펴보면서 의존성을 끝어냈지... 아이고 눈이야~]
루나🐰 : 들을땐 그럴싸한데... 이게 가능해?
루나🐰 : 뱃지 만들어달라는 요청이 왔을때 A라는 랭킹 사이트와 B라는 랭킹 사이트 어디에서 랭킹정보를 가져와야 하는지 어떻게 구분해?
나 : 컨트롤러에서 요청이 들어오면 요청 자체에 어디 랭킹 정보에 대한 요청인지 인식표를 박아!
나 : 이후 서비스에서는 요청에 박힌 인식표를 Map의 키로 사용해서 알맞는 어뎁터를 가져오는거지 🍊🍋
루나🐰 : 도메인으로 로직도 이동했고, 인프라도 감싸서 확장성도 챙겼으면 이제 리팩토링은 끝난거야?
나 : 이제 기왕 건드린김에 랭킹 뱃지 도메인의 테스트도 손봐야지!
루나🐰 : 테스트를? 여기는 그냥 돌아가는지만 테스트하면 되는거 아니야? 잘 돌아가는데 뭘 고쳐..?
나 : 음.. 우선 테스트가 너무 느렸던것 같아 의존성을 필드 인젝션했거든
나 : 그래서 스프링 컨텍스트를 테스트마다 올려야해서 단위테스트가 너무 느리더라고
나 : 테스트 용이하게 전부 생성자 주입으로 변경했고, 여러가지 "테스트 더블"을 이용해서 다양하게 테스트 했어
루나🐰 : 테스트 더블은 어떻게 이용했어?
나 : 음...🤔🤔 우선 스파이라는 친구가 있거든 애는 실행되었는지 안돼었는지 플래그를 갖고 있어
나 : 서비스 클래스를 테스트한다 하면 레포지토리 스파이를 통해 서비스가 실제로 레포지토리를 실행시켰는지 테스트했어!
나 : 그 외에도 더미나 스텁등으로 테스트할 친구들을 편하게 생성한것 같아!
나 : 사실 대부분은 하나의 테스트에서 하나만 테스트하게 변경한거지만..😅😅
(여러개 있으니까 그냥 복잡하더라고, 테스트가 쉬워야 테스트지..ㅋㅋ)
루나🐰 : 소문으로는 그런거 직접 만들었다는데... 왜 모키토 안쓰고 직접 만들었죠?!
나 : 하... 이게 나도 처음에는 그냥 라이브러리 써서 편해지고 싶었거든
나 : 우리는 랭킹 웹사이트를 빈으로 만들어서 List<RankingSite> 이런식으로 서비스 클래스에서 주입받고 있었어
나 : @PostConstruct 어노테이션을 이용해서 프라이빗한 init메소드에서 리스트의 내용을 Map에 옮겨주는 작업을 하고 있었거든
나 : 그런데 @PostConstruct 이거는 빈으로 등록된 이후에 동작하는거라 초기화 메소드가 안돌아가더라구..
나 : 그래서 초기화 메소드의 접근 제어자를 패키지 프라이빗으로 상향시켰지... 캡슐화를 깨는거라 눈물났어😭😭
루나🐰 : 다들 그렇게 하고 그냥 final 박잖아~ 스파이 직접 만든 이유나 말해줘~
나 : 앗..! 그건 그냥 별거 없어
나 : 랭킹 사이트 객체를 mock()해서 넣으니까 List에서 Map으로 변환할때 그냥 null이 들어가더라고
나 : 원래 만드는거 좋아해서 그냥 랭킹 사이트 인터페이스를 구현한 테스트 더블을 만든거지...(최근에 필터체인도 만들어 봤는데..ㅎㅎ😎)
@PostConstruct
final void init() {
webSiteMap = webSiteList.stream().collect(Collectors.toMap(
RankWebSite::getFrom,
webSite -> webSite,
(oldSite, newSite) -> newSite
));
badgeFileMap = badgeFileList.stream().collect(Collectors.toMap(
RankedBadgeFile::getFrom,
file -> file,
(oldFile, newFile) -> newFile
));
}
[나 : 이렇게 List에 담긴 친구들을 Map으로 옮겼어]
루나🐰 : 마지막으로 서비스랑 도메인에 시를 썼다는 말이 있는데 이건 무슨 일이야?
나 : "읽을만한 코드"가 우리 리팩토링의 최우선 목표였잖아
나 : 시작할때는 코드매트릭스 기준 뱃지 도메인 복잡도 점수 10점 이하가 목표였거든
나 : 근데 하면 할수록 배우는게 늘어가더라고, 멘토님께서 추천해주신 "클린 코드"라는 책에서도 그 기준을 찾아봤어
루나🐰 : 간단히 말해 "책 읽듯이 읽히는 수준"까지 했다 이거지?
나 : 맞아..ㅋㅋ 정확히는 도메인의 퍼블릭 메소드까지는 "비개발자가 읽어도 이해되는 수준"으로 만들었지
루나🐰 : 새로운 기능이 추가되는것도 아니고 왜 그렇게 했어? 시간이 남아 돌아??
나 : 우리가 지금 개발하는건 오픈소스잖아 학교 동아리 웹사이트기도 하고
나 : 누구나 개발에 참여할 수 있고, 학교가 있는 한 계속 유지되야하는 특징이 있거든
나 : 그래서 높은 수준의 가독성을 챙기려고 노력했어(외계어 적혀 있으면 거기에 기여 못하지...)
나 : 하면서 느낀건데 코드 퀄리티가 높으니까 기능 추가도 수월해졌고,
나 : 테스트 짜기도 쉬워지고, 테스트 많으니까 변경하기도 두렵지 않고, 선순환이 이어지더라구
루나🐰 : 마지막으로 그렇게 아름답다고 난리치던 코드~
public String requestRankedBadge(final RankedBadgeRequest request) {
RankWebSite webSite = getWebSite(request);
RankedBadgeFile rankedBadgeFile = getRankedBadgeFile(request);
RankedBadgeData badgeData = RankedBadgeData.of(request, webSite);
return RankedBadge.getBadge(rankedBadgeFile, badgeData);
}
나 : 처음에 무지 길었는데 감격스러워...😆🥳🤩
루나🐰 : 소감도 한번 말해줘!
나 : 이번에 리팩토링을 진행하면서 여러가지로 느낀점이 많은데 이 글을 통해 다들 나와 같은 감정을 공유했으면 좋겠어...
나 : 조잡해서 글에는 담지 못한 여러가지 내용들도 있어(토스팀의 테스트 커버리지 100%에 영감받은..ㅋㅋ)
나 : 도메인으로 나눈 부분을 레이어드 아키텍쳐로 끌고갈지 풀지못한 고민도 있어
(현재는 뱃지 밑에 어뎁터,어플리케이션,도메인 바꾼다면 어뎁터 밑에 뱃지, 어플리케이션 밑에 뱃지...)
나 : 부하를 부어보고나 이런 재밌는것도 못해봐서 아쉬운점도 있어
나 : 그래도 꾸준히 이어가는 서비스인만큼 앞으로도 어떻게 문제를 해결해가는지 응원해주세요!!🔥🔥🔥
소중한 시간에 긴 글 읽어주셔서 감사합니다🙇🏻♂️
'개발 > 알파벳-다시만들기' 카테고리의 다른 글
과거의 내가 귀여웠던 순간... (0) | 2022.09.22 |
---|---|
뱃지 서비스 리팩토링 [2], 빈으로 등록하고 캐싱곁들이기... (0) | 2022.09.18 |
뱃지 서비스 리팩토링 [1], 충격... (0) | 2022.09.11 |
재개발 계획과 진행에 대한 작은 포스팅 (0) | 2022.08.18 |
알파벳을 다시 만들어요 (2) | 2022.08.18 |