우테코 5기

[레벨 2 미션] 쇼핑 주문 협업 미션 학습 기록(2)

teo_99 2023. 6. 26. 02:03

예외 객체와 상태코드 분리

지하철 미션을 진행하면서 리뷰어님과 '비즈니스 영역의 예외 객체가 상태코드까지 갖고 있는게 맞는가' 에 대한 이야기를 여기에서 나눈 적이 있었습니다. 

 

그리고 리뷰어님이 실무에서는 어떻게 예외 객체와 상태코드를 처리하는지 보여주셨는데, enum을 통해 비즈니스 계층의 예외와 상태 코드를 표현 계층에서 매핑하고 있는 모습이었습니다.

 

좋은 방법이라는 판단이 들어서 이번 미션에서 이를 적용해보고 싶었고, enum 객체를 만들어 예외 객체와 상태코드를 공통적으로 관리했습니다. 이를 통해 ExceptionHandler에서 로깅 등의 작업에만 집중할 수 있게 되었습니다.

 

대략 아래와 같은 형태인데, 자세한 내용은 프롤로그에 작성해두어서 링크도 함께 첨부하겠습니다.

public enum ExceptionStatusMapper {

    UNAUTHORIZED(HttpStatus.UNAUTHORIZED, List.of(
            AuthenticationException.class
    )),
    FORBIDDEN(HttpStatus.FORBIDDEN, List.of(
            IllegalMemberException.class
    )),
    CONFLICT(HttpStatus.CONFLICT, List.of(
            PointInconsistentException.class
    )),
    ;

    // 프로퍼티 선언 및 생성자

    public static HttpStatus of(Exception exception) {
        return Arrays.stream(values())
                .filter(value -> value.exceptions.contains(exception.getClass()))
                .findAny()
                .orElseThrow(() -> new NoProperStatusCodeException(exception))
                .httpStatus;
    }
}

https://prolog.techcourse.co.kr/studylogs/3658

 

우아한테크코스 학습로그 저장소

우아한테크코스 크루들이 배운 내용을 기록하는 학습로그 저장소입니다.

prolog.techcourse.co.kr

 

개발용 DB

리뷰어님이 테스트 DB 와 로컬 DB 가 같은 Database URL을 보고 있다! 라고 지적해주셨습니다. 이를 통해 처음에 든 생각은,

  1. 결국 중요한 건 운영 DB인데, 로컬 DB까지 관리해야 할 필요가 있을까?
  2. 로컬 DB는 언제 사용되는걸까? 
  3. 테스트 DB와 로컬 DB를 분리하면 어떤 이점이 있을까?

였는데요.

 

하지만 제가 이전까지 간과하고 있었던 점은 로컬 DB(개발 DB)가 의외로 중요하게 사용될 수 있다는 것이었습니다.

개발용 DB(로컬 DB)가 어디에 사용될 수 있는지 대략적으로 알아봤는데 다음과 같은 사용처가 존재했습니다.

  • 데이터 구조 설계
  • 테스트 데이터 생성
  • 애플리케이션 개발 및 디버깅
  • 성능 테스트 

실제 운영 환경을 접해보지는 못했지만 '테스트용 DB로부터 분리해야 할 이유'로는 충분했습니다. 그래서 아래와 같이 테스트용 Profile과 개발서버용 Profile을 분리하게 되었습니다.

개발서버용 Profile
테스트용 Profile

 

 

Profile 분리

이번 미션에서는 배포를 진행하다보니 개발용 Profile과 운영용 Profile이 나뉘었고 이에 따라 적절한 Profile 설정이 필요했습니다. 그리고 그 과정에서 습득한 내용은 다음과 같습니다.

  • Profile을 따로 지정하지 않으면 "default"로 실행된다.
  • 인텔리제이 IDE에서 Profile을 지정할 수 있지만, 다른 컴퓨터 환경에는 적용이 안된다.
  • 따라서 Spring 설정 파일의 spring.profiles.active 속성을 이용해 사용할 Profile을 지정해주어야 한다.

그리고 추가적으로, yml 파일에서 profile을 위처럼 정의하게 되면 '운영 서버에서는 어떻게 prod Profile을 사용하는지? 라는 의문이 들 수 있는데, CLI로 jar파일을 실행시키는 경우 spring.profiles.active 옵션을 줄 수 있고 이 경우에는 CLI로 준 옵션의 우선순위가 더 높아서 yml의 내용이 덮어씌워지게 됩니다. 즉, 위처럼 구성해도 운영 환경에서는 application-prod.yml을 정상적으로 사용할 수 있습니다.

 

 

일대다, 다대일

리뷰어님이 다음과 같은 질문을 주셨습니다.

저는 CartItemMember를 참조하도록 설계한 상태였습니다. 하지만 도메인 관점에서 생각해보면 일대다 관계, 즉 Member 혹은 Cart가 여러 CartItem을 리스트로 들고 있는 형태가 맞다는 생각이 듭니다. 

 

리뷰어님의 질문을 통해 객체를 일대다 관계로 구성하는 것과 다대일 관계로 구성하는 것 사이에서 많은 고민을 하게 되었습니다. 그리고 각각의 장단점을 고민해보았는데, 다음과 같은 결론에 도달했습니다.

 

그리고 리뷰어님은 다음과 같이 답변해주셨습니다!

이를 통해 느낀 점은 다음과 같습니다.

  • 일대다, 다대일 관계에 정답은 없다.
  • 비즈니스 + 도메인 특성 + 성능을 모두 고려해 판단해야 한다.
  • 필요에 따라서는 리스트 자체를 Lazy하게 가져오나 양방향 참조를 고민해볼 수 있다.

 

 

Logging

요즘들어 로깅의 필요성을 많이 느끼고 있습니다. 이전까지는 어플리케이션을 로컬 환경에서만 실행시키기도 하고, 그렇게 오래 돌릴 일이 없었기에 로깅의 중요성을 깨닫지 못했는데 실제 배포 환경에 접어들고 나니 로깅 없이는 어플리케이션 결함을 쉽게 찾지 못하겠다는 생각이 들었습니다.

 

따라서 이번 미션에서는 logback을 사용해 파일에 로깅하도록 구성해보았습니다. 

<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>./logs/debug.log</file>
        <filter class="cart.config.LogbackDebuggingFilter"/>

        <encoder>
            <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%logger] - %msg%n</pattern>
        </encoder>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>debug-%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
    </appender>

위와 같은 형태로 logback-access.xml 파일을 구성해 로그 레벨 수준이 'DEBUG'인 친구들과 'ERROR'인 친구들을 따로 파일에 저장해두었습니다.

이렇게 구성함으로서 운영 환경을 실시간 모니터링 할 필요도 없어졌고, 필요할 때 error.log를 살펴봐 어플리케이션에 결함이 있는지를 확인할 수 있게 되었습니다. 

 

Logger name

위 logback-access.xml에서 Custom Filter를 적용했는데요(우리의 어플리케이션에서 발생한 로그만 검출하기 위해), 리뷰어님이 그걸 보고 다음과 같은 질문을 주셨습니다!

 

그리고 Logger 이름을 어떻게 지정할 수 있을지 찾아본 결과, 다음과 같은 내용을 학습할 수 있었습니다.

  • Logger 이름의 경우 생성 시점에 지정해줄 수 있다.
  • Logger 이름은 임의로 지정해도 되나, 일반적으로는 해당 Logger가 정의된 클래스의 PATH를 사용한다.
  • SpringBoot 프레임워크도 내부적으로 Logger 이름을 PATH를 통해 지정한다. 

 

 

DataAccessException은 400번대? 500번대? 

Spring에서는 @Repository 어노테이션이 붙은 객체에서 데이터베이스 관련 예외가 발생하면 DataAccessException이라는 unchecked Exception으로 전환해줍니다. 그렇다면 이 예외는 상태코드를 400번대로 처리해야 할까요, 500번대로 처리해야 할까요? 혼자서는 결론이 나지 않을 것 같아 슬랙에서 크루분들께 여쭤봤습니다.

이에 대한 크루분들의 의견을 종합해보자면 다음과 같았습니다.

  • 비즈니스 관련 예외는 '가능한' 비즈니스 로직에서 모두 검출되어야 한다.
  • 비즈니스 로직에서 놓친 비즈니스 예외는 일단 500번대로 반환한다.
  • 그리고 추후 로깅 등을 통해 비즈니스 로직을 보완한다.

그리고 저도 이 논의 과정을 통해 '400번대 예외는 비즈니스 로직에서 최대한 검출해야 한다'라는 결론에 도달할 수 있었습니다. 현실적으로 모든 비즈니스 예외를 잡는 것은 불가능하기에 로깅 같은 운영 측면의 역량이 중요할 것 같다는 생각이 드는 계기였습니다.

 

preflight와 interceptor

이번 미션에서는 사용자가 주문, 장바구니 정보에 조작/접근하려 할 때 BASIC 64로 인코딩된 Authorization 헤더가 필요했습니다. 그리고 이 인증 절차를 저는 Interceptor에 구현했는데요, 실제 배포 과정까지 거치자 예상하지 못한 문제가 발생했습니다.

 

바로 preflight 요청 때문입니다. preflight 요청이란, CORS의 한 동작 중 하나로 미리 예비요청을 보내 '진짜 데이터를 보내도 되는지' 확인하는 과정입니다. 하지만 이 preflight 요청의 헤더에는 Authorziation 헤더가 없고 당연히 예외가 발생하게 됩니다.

 

따라서 다음과 같이 Interceptor 코드를 변경해주었습니다. 

 preflight 요청인 경우에는 OPTIONS 메소드를 사용하기 때문에 request Method가 OPTIONS라면 바로 Interceptor를 생략하게 해줬습니다.