[프로젝트] Live-Session Exception/Log

paduck·2023년 4월 8일
0

프로젝트

목록 보기
10/11

개인적으로 log를 찍어서 해당 내용을 확인해보고 싶은 욕구가 생겼다
그런데 무지성 log만 찍으면 의미가 없을 것 같아서, 하는 김에 에외 처리 나 에러 처리 같은 것도 해보고 싶어가지고 작성을 시작하게 된 포스팅이다

현재 프로젝트에서 Live-Session 과 알림 처리를 담당하고 있는데,
실질적으로 세션은 사용자와 처리하는 부분이 크고
알림은 서버 내에서 처리되는 부분이 커서 뭔가 상황별로 학습하기 좋을 것 같아 따로 진행해볼 예정이다

Log 찍기, 에러/예외 처리 하기

  • MVC 패턴을 적용해서 그에 맞게 쓸껀데 결국 다른 상황에서도 비슷하지 않을까?

Controller

  • 요청(Request)을 받아들이고 처리한 결과(Response)를 반환하는 역할을 합니다.
  • 즉, 클라이언트와 서버 간의 통신을 담당하며, 사용자 입력을 검증하고 요청을 처리하기 위해 서비스 계층에 전달하는 역할을 합니다.
  • 주로 사용자 입력에 대한 검증 및 서비스 계층에서 전달받은 결과에 대한 처리를 포함합니다.
  • 반면, 컨트롤러 계층에서 발생하는 예외 처리는 주로 요청 및 응답 처리와 관련된 것들로 한정되기 때문에, 서비스 계층에 비해 예외 처리의 범위가 상대적으로 작습니다.

Service

  • 비즈니스 로직을 처리하는 역할을 합니다.
  • 데이터의 생성, 조회, 수정, 삭제 등의 연산을 수행하며, 이러한 동작에 대한 검증 및 예외 처리를 담당합니다.
  • 주로 데이터 처리 과정에서의 오류, 무결성 오류, 서버 간 통신 오류 등을 포함합니다.
  • 서비스 계층에서 발생하는 예외 처리와 로그를 작성하는 것이 일반적인 원칙이 되었습니다.
    - 서비스 계층은 비즈니스 로직을 처리하기 때문에 여러 상황에서 예외 및 오류가 발생할 가능성이 높습니다.
    • 또한 서비스 계층에서 로그를 작성하면 디버깅과 오류 추적이 훨씬 용이해지며, 코드의 가독성과 유지 보수성이 향상됩니다.

로깅 과 예외 처리

가장 먼저 생각이 들었던 건 예외 처리였다
결국, 서버에서 오류가 발생했을 때 클라이언트에게 어떤 오류인지 알려줄 수 있어야 하며, 오류가 발생해도 서버가 멈추어 버리지 않고, 예외 처리한 이외의 곳은 돌아갈 수 있게 구현을 해야 할 것 같다는 생각이 많이 들었다.

그런데, 여기서 더욱 중요한건 예외 처리만 하고 끝이 아니라는 생각이었다.
백날천날 예외처리 등록해봤자 서비스를 개발한 당사자가 하루종일 컴퓨터 앞에 앉아서 에러 내역을 뚫어지게 쳐다보고 있는 것이 아닌데 도대체 어떻게 이걸 확인할까?
그렇다 보니, 자연스레 로깅 역시 필요하다 라는 생각을 많이 하게 되었다.

사용 과 구현

그러면 어떻게 로깅을 구성할까?
어떻게 예외/오류 처리를 할까 생각했는데 생각보다 답은 쉬웠다.
여러 가지 방법이 있겠지만, 로깅은 로깅 라이브러리를 사용하고 예외 처리에는 예외를 발생시켜 전역 예외 처리기를 하나 둔 다음에 거기서 세분화 시키는 형태로 진행하기로 했다.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
    <Appenders>
        <RollingFile name="RollingFile"
                     fileName="./logs/${date:yyyy-MM}/app-${date:yyyy-MM-dd}.log"
                     filePattern="./logs/${date:yyyy-MM}/app-%d{yyyy-MM-dd}-%i.log">
            <JsonLayout compact="true" eventEol="true" properties="true"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="10 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="RollingFile"/>
        </Root>
    </Loggers>
</Configuration>

개인적으로 json을 프로젝트 할 때마다 사용해왔다 보니까 json이 편했고, 무엇보다 읽기에는 불편하지만 추후에 어떠한 로그 분석 도구나 다른 서비스에 적용해서 파싱 작업을 할 때 추가적인 변환 작업이 불필요하기 때문에 미리미리 연습하기로 했다

왜 전역 예외 처리?

가장 간단하게 생각할 수 있는건 try...catch 구조를 통해 처리하는 것이다
근데 코드를 작성하다 보니까 try...catch 형태로 진행하면 코드가 엄청 길어진다는 불편함이 생겼다.
개인적으로 처음 로직을 작성할 때는 무작정 쓰고 추후에 생각하며 리펙토링 하는 걸 선호하는데, 때마침 try...catch로 동일한 구조지만 반복에서 코드가 쓰이고, 서비스 계층에서는 에러를 포함해서 반환하다 보니 코드도 엄청 길어지는게 너무 거슬렸다.
그래서 찾아본게 전역 예외 처리다.
throws Exception을 던져주고, 전역 예외 처리기에서 예외 상황에 따라 어떤 식으로 로깅하고, 예외 처리를 발생할지 작성해주기만 하면 되서 넘모 간단해보였다.

사용 과 구현

전역 예외 처리 핸들러를 만들었고, ResponseDTO라는 값으로 원래는 던져주고 있어서 그와 비슷한 형태가 될 수 있게 http status 랑 body를 활용했다. 다 쓰기에는 내용이 길어지니까 일부만 가져왔고, 결국 형태는 동일하니까...

// redis 관련 오류 처리
    @ExceptionHandler(RedisConnectionFailureException.class)
    public ResponseEntity<\?> handleRedisConnectionFailureException(RedisConnectionFailureException e){
        logger.error("Redis 연결 오류: ", e);
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("Redis 연결에 오류가 발생했습니다. "+e.getMessage());
    }
 
    @ExceptionHandler(RedisException.class) //추가 예외 처리
    public ResponseEntity<?> handleRedisException(RedisException e) {
        logger.error("Redis 오류: ", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Redis에서 오류가 발생했습니다. " + e.getMessage());
    }

엥...velog에서 오류가 발생해서, \를 추가하였습니다만 아무 타입이나 쓰려고...

컨트롤러 레이어 이외에서...?

결국 필요한 부분에서 로깅을 처리하면 해당 예외에 대한 적절한 로깅이 가능하니까 분리해서도 진행하는 것 같다. 가장 큰 목적은 오류 해결이니까 말이다.
또 서비스에서도 하고, 전역 예외로 예외를 던져서 처리하면 예외 값에 대해서는 전역 예외 처리기를 통해 일관된 형태를 받을 수도 있으니까 추후 로그 분석 등에서는 커다란 불편함이 없을 수 있어, 단점보다는 장점이 많아 이용되는 것 같다

로깅 처리야 어디서 하든 logger를 쓰고 있으니까 간단한데 그러면 예외 처리는 어떻게 해야할까?
이제 여기서 선택지가 2개로 나뉜 것 같다.
spring boot를 쓰고 있으니까 해당 프레임워크가 제공해주는 기본 에러를 반환하거나(전역 처리기에 등록되어 있으면 이리로 간다), 아니면 사용자 정의 예외 클래스를 만들어서 조금 더 명시적으로 오류를 지정해서 처리하는 방법이다
사용자 정의 예외 클래스는 좀 더 찾아보거나 해야 할 것 같고, 현재 서비스에서는 굳이 불필요 한 것 같아서(다른 서비스에는 필요해서 쓸지도) 넘겼다

...
public SessionRedisFindAllResponse list(){
        try{
            // 1. Redis에서 모든 채팅방 정보를 가져온다.
            Set<Object> rooms = redisTemplate.opsForHash().keys("rooms");
            Map<Object, Object> roomsInfo = new HashMap();
            for(Object room : rooms){
                roomsInfo.put(room, redisTemplate.opsForSet().members(room.toString()));
            }

            if(rooms == null || rooms.isEmpty()){
                logger.error("Redis 세션 전체 출력 오류! ");
                throw new IllegalArgumentException("현재 Redis 세션이 없습니다. ");
            }
            // 2. Redis에 있는 모든 채팅방 정보를 응답해야 한다.
            SessionRedisFindAllResponse response = SessionRedisFindAllResponse.builder()
                    .code(HttpStatus.OK.toString())
                    .msg("정상적으로 처리되었습니다.")
                    .data(roomsInfo.toString())
                    .build();
            return response;
        }catch(RedisException e){
            logger.error("Redis 세션 전체 출력 오류! ",e);
            throw new IllegalArgumentException("Redis 세션 전체를 불러오는데 오류가 발생했습니다. ",e);
        }
    }

테스트

...
@Test
    public void removeSuccess() {
        // Given
        sessionService.enter(new SessionRedisSaveRequest("test", 1L, 60L));

        // When
        SessionRedisRemoveResponse response = sessionService.remove("test");

        // Then
        assertNotNull(response);
        assertEquals("정상적으로 처리되었습니다.", response.getMsg());
    }

    @Test
    public void removeFail() {
        // Given
        SessionRedisRemoveRequest request = new SessionRedisRemoveRequest(null);

        // Then
        assertThrows(IllegalArgumentException.class, () -> sessionService.remove(request.getRoomId()));
    }

서비스 계층에서 예외 처리를 구현하였다
예외 처리를 구현하면서 조금 스트레스 받았던건 ResponseEntity를 반환하는 형태를 변화시키지 않아야 하다 보니까, Controller 단에서 비슷비슷하게 예외 처리를 구현할 수 밖에 없었던 점이다.
그리고 구현하면서 아직도 잘 되지 않은건, 분명 특정 Exception을 던지게 설정한 것 같은데 계속해서 4xx 클라이언트 예외가 발생했다.
해당 부분은 추후에 시간을 들여서 수정해야 될 것 같다

변동 사항

.yml 로 파일 변경

  • application.yml 파일 수정
    log4j2 에 대한 파일 참조 경로 설정
logging:
	config: classpath:log4j2.yml
  • log4j2.yml 파일 작성
    설정 파일에 대한 통일성을 위해 작성
Configuration:
    status: info

    Properties:
        Property:
            name: log-path
            value: "developers-live-session"

    Appenders:
        Console:
            name: Console_Appender
            target: SYSTEM_OUT
            PatternLayout:
                pattern: '%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n'
        RollingFile:
            name: File_Appender
            fileName: ./logs/${log-path}.log
            filePattern: "./logs/${log-path}.%d{yyyy-MM-dd}.log"
            JsonLayout: // 해당 부분은 일반적으로 Pattern을 사용하나
            //추후, 로깅 검색 등의 작업에서 전달 및 파싱의 편의를 위해 json 사용
                compact: true
                eventEol: true
                properties: true
            Policies:
                TimeBasedTriggeringPolicy:
                    modulate: true
                    Interval: 1
            DefaultRolloverStrategy:
                max: 5
                fileIndex: min

    Loggers:
        Root:
            level: info
            AppenderRef:
                - ref: Console_Appender
                - ref: File_Appender
profile
끈질기게 들러붙기

0개의 댓글