240509 Spring 숙련 - 과제 발제 보고 공부하기

노재원·2024년 5월 9일
0

내일배움캠프

목록 보기
35/90

Spring 숙련 과제 발제

오늘부터 숙련 공부하는 기간이긴 하지만 어쨌든 어제부터 진행했고 아직 강의를 20%도 나가지 못했는데 과제를 먼저 보니 이번에 구조적으로 고민할 부분을 잘 찾아야겠다는 게 느껴졌다.

강의에서 배운 내용을 토대로 TODO List 서버를 만드는게 과제인데 얼추 이유를 알던 App Native때와 달리 이유를 모르고 과제 때문에 정확히 모르는 데 붙여 쓰는 불상사가 일어날까 조금 무섭기도 하다.

아마 모르는 걸 억지로 쓰기 시작하면 과거 행보를 다시 밟는 미숙한 개발자가 될 것 같으니 이번엔 부족하더라도 챗봇, 팀, 튜터님 모두에게 도움을 받아가며 그 이유를 정리해나가야 할 것 같다.

기능 구현까지는 어찌저찌 할 수 있겠지만 이번 TODO List 과제의 목표 추가 달성 목표를 설정해보기로 했다.

  • ERD, Diagram등도 설정해서 설계부터 차근차근 접근하기
  • Delete는 진짜 제거가 아니라 Status로 제거 관리하기
  • created_at, update_at, deleted_at 등 Date 처리 잘 하기
  • Docs 작성 잘 하기
  • 로그 잘 남기기
  • User / Admin 구분해보기
  • 혹시 된다면 Production / Test 환경 분리하기
  • 비로그인 계정이라 치고 아이디 / 비밀번호를 받아 관리하는데 암호화 잘 하기?
  • Scheme 명시하고 실무에서도 범용적으로 그렇게 쓰는지 확인하기
  • Validation 잘하기
  • Status code, Message 관리 잘하기
  • Query parameter, Path variable 관리 잘하기
    resource를 식별하고 싶으면 Path Variable을 사용하고
    정렬이나 필터링을 한다면 Query Parameter를 사용하는 것이 Best Practice
  • v1 으로 관리 시작하기
  • 내가 앱에서 써야 한다면? 이라는 생각으로 접근하기
  • retool 은 써봐도 좋고 타임리프는 금지다
  • Pagination, N + 1 Query, Auth

일단 발제만 보고는 이 정도고 아마 강의가 한참 남았으니 들으면서 내용을 많이 추가하게 되지 않을까 싶다. 그런데 이미 너무 많은 거 아닌가? 강의도 다 안봤는데 약 10일간 만들 거 치고는 내용이 많은 것 같다. 그냥 희망 사항인 걸로 해야겠다.


여담인데 이번 과제 말고 추후 과제는 Html/css/js도 같이 반환한다고 한다. View까지 그리면 난 View가 더 어려울 것 같다.

Spring 숙련 강의 이어가기

과제가 꽤 분량이 많아 보여서 가늠이 안되가지고 강의를 최대한 빨리 수강하고 실전을 곁들여서 공부하기로 했다. 그래서 오늘은 강의를 좀 빨리 뺐다.

강의를 보며 느낀 건데 앱도 구조를 처음부터 설계 잘해야 편하긴 했지만 Web application은 Refactoring 하기 조금 더 빡실 것 같다. 다만 앱 구조가 상대적으로 쉬운건지 못짜서 코드 복잡도가 적었다고 봐야하는지는 불확실하긴 하다.

아키텍처

  • 내가 앱 개발할 때 쓰던 기능별로 모아둔 패키지 구조가 모놀리식 아키텍처였다
    • 특정 서비스에 몰리면 분산하며 Service oriented architecture 로 구성하게 된다
      • ESB 같은 걸로 서비스를 통합하지만 ESB도 결국 커지게 된다, SOA는 잘 안쓰고 바로 MSA로 넘어감
    • 모든 서비스를 더 작게 쪼개서 구현한게 Micro service architecture다
  • 비즈니스 로직 파악을 쉽게 하기 위해 Domain 별로 묶는게 좋다

DTO

  • Data class는 Destructuring(componentN)을 지원하고 이걸로 Property에 접근하기가 굉장히 용이하다


    Mapping
  • @RequestMapping을 사용하면 Http request를 가로챈 DispatchSublet이 HandlerMapping를 통해서 해당 URL에 매칭된 적절한 Controller를 찾고 실행함

Service 작성

  • 각 도메인의 Controller, DTO 작성을 마쳤으면 Web(Presentation) layer 작성을 마친 것
  • Service는 작업 단위이며 그렇기에 Service에 Transaction 을 건다고 볼 수 있음
    • Transaction은 작업을 묶는 논리적인 실행 단위, 모두 성공해야만 커밋한다
      - 원자성, 일관성, 고립성, 지속성 (정보처리기사 ACID)
      • 고립성은 Level 을 조회할 수 있고 Read Commited를 기본 레벨로 가진다.
        커밋되기 전엔 커밋 전 상태를 조회한다, 커밋된 상태만 조회한다
  • Service interface와 ServiceImpl class를 구분하면 리뷰도 쉽고 주입도 Spring이 관리해서 Bean을 찾는다.
    • 주입이 쉽고 추상화 적용이 되지만 코드 복잡도가 증가하고 아주 간단한 로직에서 오버헤드가 증가하고 지나친 추상화가 될 수 있으니 주의
      • Impl 패턴을 사용할 지는 1. 서비스 계층 복잡도 2. 확장성 3. 코드 복잡도를 고려하는게 좋다.
      • 1:1 매칭이 아닌 경우엔 User를 예로 들면 데이터 접근 방식이 다른 UserDBImpl, UserAPIImpl 로 구현체가 2개 필요한 경우나 Production, Test 등 환경을 바꿀 때 환경별 Impl을 할 수도 있고 상품의 할인률을 달리 해야하는 Service 구현체가 여러개 있을 수 있는 등 Impl 패턴은 추상화가 잘 되어있다.
  • Response body를 필요로 하지 않으면 ResponseEntity.build() 를 호출한다.
  • 선택의 문제지만 같은 Aggregate 내의 Service라면 Service를 분리해서 구현하지 않고 Aggregate 최상위 Service에서 처리되게 할 수도 있다.
    • 정답은 없고 상위 Service 에 하위 여러개를 주입할 수도 있고 하위 Service에 상위 Service를 주입받는 것도 가능하지만 상위 Service에 주입하는 것이 DIP, SRP를 준수해서 일반적으로 옳다.
      - 상위 Service 에서만 처리하면 볼륨이 커질 수도 있으니 분리하는 것도 할 줄 알아야 한다.

예외 처리

  • id 기반 예외, body 예외등 ServiceImpl 에선 다양한 예외를 처리해야 한다
    • CourseNotFoundException 같은 구체적인 Exception도 좋고 ModelNotFoundException도 좋다
      • 범용적인 Exception이라면 domain 하위에 exception package 에서 생성해 관리해도 된다.
        • Application 실행 중 발생할 수 있는 예외니 RuntimeException 상속을 추천한다.
    • ServiceImpl의 각 메소드에서 먼저 TODO로 예외 케이스를 작성하는 것도 중요하다
    • 아무런 조치도 없는 Exception은 Spring의 ErrorController가 처리한다.
    • 예외 응답은 처리가 Service 에서 작성됐지만 Status와 함께 핸들링은 Controller 에서 처리하고 따지자면 Web(Presentation) Layer에 속한다
    • Controller 에서 @ExceptionHandler Annotation 으로 Exception을 지정하면 Exception이 떨어지면 해당 함수를 실행한다
      • 이러면 Controller별로 Exception 핸들링이 중복될 확률이 높아서 전역적으로 처리하게 @ControllerAdvice, @RestControllerAdvice 어노테이션을 사용할 수 있다. (MVC 기반으로 View를 뿌려주는지 아니면 Rest하게 JSON 응답만인지의 차이)
        • Advice annotation을 사용하는 Controller는 여러 개 만들 수 있으니 사이즈가 커지는걸 대비해야 한다.
    • Exception response message도 DTO로 만들어 반환한다.
    • Status code 말고 Error response에 Error code를 추가해서 Enum으로 관리해주면 에러를 더 명시할 수 있다

Transaction

  • @Transactional은 롤백이 필요할 수도 있는 데이터의 변화가 확실할 때 걸어줘야 한다 (CUD Interface)
    • Transactional 은 DB와 연결한 Driver가 필수다
      • H2 DB는 Spring 실행 시 DB도 같이 떠서 테스트에 쓰기 좋은 DB다

코드카타 - 프로그래머스 카드 뭉치

fun solution(cards1: Array<String>, cards2: Array<String>, goal: Array<String>): String {
    var answer = ""
    val cards1List = cards1.toMutableList()
    val cards2List = cards2.toMutableList()
    val goalList = goal.toMutableList()
    var breakFlag = false
    
    while (goalList.isNotEmpty()) {
        val goalKeyword = goalList.removeFirst()
        if (cards1List.getOrNull(0) == goalKeyword) {
            cards1List.removeAt(0)
        } else if (cards2List.getOrNull(0) == goalKeyword) {
            cards2List.removeAt(0)
        } else {
            breakFlag = true
            break
        }
    }
    
    answer = if (breakFlag) "No" else "Yes"
    return answer
}

처음에 문제를 읽고 cards1, cards2를 교차로 묶어서 처리하면 되는 줄 알고 flatMapIndexed 를 써서 묶었는데 문제를 잘못 읽은걸 뒤늦게서야 깨달았다.

요지는 순서대로 사용해야한다는 점 같아 첫 번째 값만 지속적으로 비교하는 알고리즘을 떠올렸고 순회 중단 시점도 명확하게 순서대로 구분해도 문장을 이룰 수 없을 시 탈출하는 것이니 "No"를 반환하고 goalList가 비었다면 "Yes" 인 걸로 작성해서 제출했다.

그런데 테스트 케이스중 한 개가 실패가 뜨길래 대체 뭘까 싶어서 고민하며 문제를 읽다가

goal의 길이 ≤ cards1의 길이 + cards2의 길이

이걸 보고 탈출 시점에 goal의 길이로 체크하는 건 명시적이지 않을 수 있겠다 싶어서 breakFlag를 만들어서 순회 탈출 유무를 체크했더니 무사히 통과할 수 있었다.

안되는 케이스가 정확히 어떤 건지는 파악 못했지만 길이에 따른 반례가 생길 수 있는건 다른 사람들의 질문을 보고 알 수 있었다.

그리고 다른 풀이를 보다가 아차차 싶었던 부분이 추가적으로 breakFlag를 쓸게 아니라 else 에서 그냥 return "No" 를 하는게 더 깔끔했을텐데 싶어서 조금 아쉬움이 남는다.



추가로 가장 깔끔한 다른 풀이를 가져왔다.

fun solution2(cards1: Array<String>, cards2: Array<String>, goal: Array<String>): String {
        var idx1 = 0
        var idx2 = 0
        goal.forEach {
            if (idx1 < cards1.size && it == cards1[idx1]) idx1++
            else if (idx2 < cards2.size && it == cards2[idx2]) idx2++
            else return "No"
        }
        return "Yes"
    }

이걸 보니 내가 왜 처음에 goal에 removeFirst를 해야하니 while 순회를 선택했는지 잘 모르겠다.

그리고 List가 제거되는 시간 복잡도도 고려하지 않아도 되는 idx를 두개를 같이 돌리는 방식을 선택했고 removeFirst로 goal의 키워드를 확인할 필요도 없었는데 내 풀이는 문제 풀이 자체는 맞는 방향으로 접근한 것 같지만 코드 퀄리티로는 너무 부족한 풀이가 됐다.

이번엔 시간이 좀 걸려서 그대로 제출한 느낌도 있는데 다음에는 제출 잘 되는 것 같으면 줄여볼 방법도 같이 고민을 해야겠다.

0개의 댓글