개인적으로 log를 찍어서 해당 내용을 확인해보고 싶은 욕구가 생겼다
그런데 무지성 log만 찍으면 의미가 없을 것 같아서, 하는 김에 에외 처리 나 에러 처리 같은 것도 해보고 싶어가지고 작성을 시작하게 된 포스팅이다
현재 프로젝트에서 Live-Session 과 알림 처리를 담당하고 있는데,
실질적으로 세션은 사용자와 처리하는 부분이 크고
알림은 서버 내에서 처리되는 부분이 커서 뭔가 상황별로 학습하기 좋을 것 같아 따로 진행해볼 예정이다
가장 먼저 생각이 들었던 건 예외 처리였다
결국, 서버에서 오류가 발생했을 때 클라이언트에게 어떤 오류인지 알려줄 수 있어야 하며, 오류가 발생해도 서버가 멈추어 버리지 않고, 예외 처리한 이외의 곳은 돌아갈 수 있게 구현을 해야 할 것 같다는 생각이 많이 들었다.
그런데, 여기서 더욱 중요한건 예외 처리만 하고 끝이 아니라는 생각이었다.
백날천날 예외처리 등록해봤자 서비스를 개발한 당사자가 하루종일 컴퓨터 앞에 앉아서 에러 내역을 뚫어지게 쳐다보고 있는 것이 아닌데 도대체 어떻게 이걸 확인할까?
그렇다 보니, 자연스레 로깅 역시 필요하다 라는 생각을 많이 하게 되었다.
그러면 어떻게 로깅을 구성할까?
어떻게 예외/오류 처리를 할까 생각했는데 생각보다 답은 쉬웠다.
여러 가지 방법이 있겠지만, 로깅은 로깅 라이브러리를 사용하고 예외 처리에는 예외를 발생시켜 전역 예외 처리기를 하나 둔 다음에 거기서 세분화 시키는 형태로 진행하기로 했다.
<?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 클라이언트 예외가 발생했다.
해당 부분은 추후에 시간을 들여서 수정해야 될 것 같다
logging:
config: classpath: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