240514 Spring 숙련 - Todo 서버 계층 만들기

노재원·2024년 5월 14일
0

내일배움캠프

목록 보기
39/90

어제까지 다 짜놓은 기본 명세서, ERD를 보고 직접 계층별로 나눠서 짜보는 시간이 왔다.

강의를 정말 많이 분석하며 봤지만 워낙 새로 배운 키워드가 많아서 한 방에 잘 될리는 없고 이제부터가 진짜 챗봇과 Copilot의 도움을 받아서 내가 생각하는 이유와 구조를 대조하며 방법을 찾아나가는 시간이다.

확실히 모르는 걸 작성한다고 생각하니 머리가 약간 열이 오르는 느낌이 있다. 하필이면 잠도 좀 설친 날이라서 유독 과열 상태다.

Presentation Layer

강의처럼 Presentation Layer 먼저 작성하면서 시작하기로 했다. 챗봇이랑 대화해보면 DDD 기반 설계는 DDD 부터 짜는 것이 일반적이라는 답변을 받았는데 강의나 튜터님과의 대화에서 보면 뭔가 정해지지 않은 객체를 가정하고 틀을 작성하는 방식이 많아서 이번엔 나도 그대로 따라가기로 했다.

마침 딱 어제 배운 객체지향의 설명대로면 Presentation -> Service -> Domain -> Infra 순서로 의존하게 구성이 되어있는 만큼 위임할 객체의 추상적인 내용 중 어떤게 필요한가를 구분하는 연습도 해봤고 사실 그래봤자 Todo 앱이라 강의랑 크게 다를 바 없었지만 작성하는 방법의 폭 자체를 늘렸다는 느낌이다.

다만 약간 에러나는 게 불편하긴 해서 빌드 돌려본 건 아니지만 그냥 Domain 부터 작성하는것도 나쁘지 않을 것 같고 그냥 계층별 로직의 흐름을 파악하며 작성하고 싶으면 Presentation layer 부터 작성하는 것도 괜찮을 것 같다.


추가로 고민하던 내용은 DTO에 Entity에서 사용하는 Enum이 그대로 쓰여도 되는가? 였다. 일단 챗봇 피셜로는 권장하지 않는 방법이라고 하긴 했는데 Status의 도메인이 3개로 한정되어 있으니 이걸 Entity, DTO가 공유해도 되지 않나? 라는 생각이었다.

챗봇이 권장하지 않는 이유와 내 짱구를 종합해서 원인을 찾아보면
1. Entity 에서 사용중인 Enum이 변경되면 그대로 DTO에도 전파되므로 의존성이 생김
2. 비즈니스 로직을 처리할 때 DTO는 최종 생성물일 뿐 DTO 자체로 데이터 제어, 변경을 할 일은 만들지 않는게 좋음
3. 패키지 측면에서도 Model에 들어있는 놈이 튀어나오는 거라 깔끔해보이지 않음

어쨌든 DTO의 status는 얌전히 String으로 처리하기로 했다. 사실 강의는 진작 이렇게 해서 그냥 얌전히 강의 따라가면 되는 거긴 했다.

Service Layer

Service layer에서 고민한 건 Todo와 Comment는 사실상 Todo aggrement에 포함되어 있으니 강의처럼 Comment의 서비스도 Todo에서 구현하는게 좋은가? 였다.

개발 편의성은 굉장히 높아질 거고 어차피 같은 Aggrement니 구조적인 측면에서도 말이 안되는 건 아닌데.. 싶었는데 앱 개발할 때도 신경쓰고 싶었던 코드(관심사)의 분산을 신경쓰고 또 단일 책임 원칙을 위반하는 느낌 같기도 해서 분리하기로 했다.

더 찾아보면 한 서비스에서 Todo, Comment를 둘 다 처리하기 위해 두 Repository를 주입받는 건 위에서 말했듯 단일 책임 원칙을 위반한다고 해석할 수 있고 간만에 들여다 본 Stackoverflow의 답변에서도 한 Service가 여러 Repository를 의존중이라면 구조에 하자가 있다고 의심해볼 필요가 있다는 답변이 있기도 했다.

이렇게 해서 거의 붙어다닐 Todo, Comment를 분리하는 게 Aggregate의 관점에선 깔끔하지 않을 수도 있고 개발 편의적으로도 괜히 먼 길 돌아갈 수도 있지만 나름의 이유가 있는 분리라고 생각하니 앞으로도 분리하면서 부딪혀보기로 했다.

Domain Layer

뼈대 구축에는 사실 제일 쉽지 않았나 싶다. 의존 관계의 최상위라 Entity를 작성하는데에 그쳤는데 양방향이 아닌 단방향 ManyToOne 으로만 구성해 맵핑하기로 해서 그 점만 신경썼다.

우선 쿼리의 예상이 쉽다는 점과 구조가 단순해서 그럴 일은 없겠지만 순환 맵핑의 방지, 연관 관계의 주체를 명시적으로 사용하고 전파로 영속성을 관리하는 가장 깔끔한 구조라고 생각한다.

물론 기왕 지원하는 기능인 거 양방향도 염두에 뒀고 양방향을 써서 코드 복잡도를 압도적으로 줄일 수 있다면 쓰는 것도 좋다고 생각한다.

추가로 @CreationTimestamp, @UpdateTimestamp 가 존재하는 것도 알아서 원래 직접 집어 넣으려던거 수고를 덜게 됐다.

그래도 개발하던 도중에 예상 외로 Cascade 부분에서 상상력을 발휘하기가 어려웠는데 @ManyToOne 관계에서 Cascade를 할 일이 필요한가? 부분에서 그러했다.

이게 클라이언트를 먼저 해봐서 그런지 '엥? 그런데 Comment 삭제 되면 Todo 카드에서도 바로 날리게 반영해야 하지 않나?' 생각이 들었는데 생각해보면 애초에 Todo는 Comment의 정보를 하나도 모르니 염두할 부분이 아니었고 변경 사항은 클라이언트에서 처리하는게 일반적일 것 같다.


인프라는 아직 작성한게 Swagger config 그대로 긁어온거 빼면 없는데 좀 쓸만한 거 있나 찾아봐야겠다.

DB 테이블도 생성하긴 했는데 이걸 ERD Cloud의 MySQL 쿼리를 PostgreSQL로 바꿔서 supabase에서 만들었는데 뭔가 잘 안되기도 했다.

생각해보면 강의를 따라갈 때 DB 테이블을 이렇게 다 만들어줬었나 하고 보니 Entity를 다 작성하고 나서 실행할 때 hibernate 에서 자동으로 테이블까지 다 만들어준다고 해서 진짜 Entity 작성에 자신 있으면 이렇게 따라가도 될 것 같긴 하다.

그래서 화끈하게 기껏 만든 테이블 다 날리고 빌드 하고나서 스키마가 맞는지 확인해보기로 했다.


코드카타 - 프로그래머스 소수 만들기

주어진 숫자 중 3개의 수를 더했을 때 소수가 되는 경우의 개수를 구하려고 합니다. 숫자들이 들어있는 배열 nums가 매개변수로 주어질 때, nums에 있는 숫자들 중 서로 다른 3개를 골라 더했을 때 소수가 되는 경우의 개수를 return 하도록 solution 함수를 완성해주세요.

문제 링크

fun solution(nums: IntArray): Int {
    var answer = 0
    val primeNumMap = mutableMapOf<Int, Boolean>()
    for (i in 0 until (nums.size - 2)) {
        for (j in (i + 1) until (nums.size - 1)) {
            for (k in (j + 1) until nums.size) {
                val num = nums[i] + nums[j] + nums[k]
                if (primeNumMap[num] == true) answer++
                else if (isPrime(num)) {
                    primeNumMap[num] = true
                    answer++
                }
            }
        }
    }
    return answer
}
    
fun isPrime(num: Int): Boolean {
    if (num == 1) return false
    
    for (i in 2 until num) {
        if (num % i == 0) return false
    }
    
    return true
}

문제는 되게 자주 보던 유형이고 그냥 평범한 알고리즘이었던 것 같다.

그렇기에 문제였는데 그냥 3중포문 딸깍하고나서 진짜 이대로 제출해야하나? 를 많이 고민해야했다.

일단 내가 알고 있는 수식으로는 이것보다 최선이 있을까가 의심이 돼서 마땅한 방법이 안떠올랐고 isPrime에 어설프게 제어문을 늘려봤자 핵심이 되는 루프문은 3중포문에서 발생하는 거니 최적화를 할 방법을 많이 생각해야했다.

그래서 잠깐 소수를 찾는 방법들을 조사했는데 그 중 발견한 게 에라토스테네스의 체 였다.

처음에는 이걸로 미리 범위를 잡아놓으면 순회도 줄고 좋겠다! 생각이 들었는데 생각해보면 문제의 스펙과 상당히 깊은 연관이 발생할 거라서 생각을 접게 됐다.

우선 nums 말고 코드에서는 주어진 정보가 없으니 nums.max와 바로 밑 값을 조사하거나 그냥 문제에 제약조건이 있으니 무시하고 num의 최대 값인 1000을 맞춰서 최대 값 998+999+1000인 2997 범위 내에서 미리 소수 표를 만들어놓고 map으로 판별한다 같은 생각은 nums size를 4개만 줘도 똑같은 낭비를 발생시킨다.

그래서 그냥 제출해야하나 고민해야하던 찰나에 에라토스테네스의 체 방법을 약간 응용해서 같은 숫자를 판별할 일이 생겼을 때를 방지하기 위한 map을 추가해서 이미 소수인지 판별했던 숫자를 검증할 때는 바로 통과하게끔 만들었다.

추가적인 변수 사용으로 괜히 코드가 더 복잡해지는 거 아닌가 생각도 들지만 퍼포먼스 상으로는 약 1ms 정도 시간을 줄였고 그냥 좋은게 좋은거지 싶기로 했다.

별개로 놓친 부분이 있었는데 소수 최대 탐색 범위를 Math.sqrt(num.toDouble()).toInt() 로 root값 미만 까지만 탐색해도 소수인 걸 판별 가능하다고 검색에서 봤었는데 이걸 안써먹었다. 아마 이것까지 썼으면 조금 더 좋았을 것 같다.

다른 제출 코드랑도 사실 핵심은 딱히 바뀐 게 없어서 조금이라도 고민한 티를 냈다는 정도로 만족하기로 했다.

0개의 댓글