개발 slecs

판다라이브 스트리밍 보안 기능 개선

목차

판다라이브는 스트리밍 특성상 요청이 짧은 시간에 몰리고, 클라이언트가 보내는 파라미터를 서버가 그대로 받아 처리하는 흐름이 많다. 그 흐름 중 일부에서 파라미터 검증이 충분하지 않거나, 내부 처리 로직이 예외 케이스를 제대로 핸들링하지 못하는 부분을 이번에 손봤다.

작업 규모는 크지 않았다. 내부 클래스 수정이 주였고, 거기서 파라미터 바인딩과 비즈니스 로직을 같이 정리했다. 그런데 이런 작업이 오히려 더 신중하게 해야 한다고 생각한다. 기능을 새로 추가하는 것보다, 기존 흐름 중간을 건드리는 게 영향 범위 파악이 훨씬 어렵기 때문이다.

파라미터 검증을 내부 클래스에서 처리하는 구조

Spring MVC + MyBatis 조합에서 내부 클래스는 컨트롤러 안에 정적 내부 클래스(static inner class) 형태로 DTO 역할을 하거나, 서비스 레이어 안에서 요청 처리를 캡슐화하는 용도로 쓰인다. 판다라이브 코드베이스도 이 패턴을 따르고 있어서, 요청 파라미터 바인딩과 실제 처리 로직이 내부 클래스 안에 몰려 있는 구조였다.

문제는 이 구조에서 검증 로직이 흩어지기 쉽다는 것. 컨트롤러에서 한 번 걸러야 할 것을 서비스 내부 클래스에서 또 걸러야 할 때, 어느 쪽에서 책임지는지가 불명확해진다. 이번 수정에서는 바인딩 시점에서 null과 빈 값 처리를 명확히 하고, 비정상 입력에 대한 처리를 내부 클래스 진입 전에 끊도록 정리했다.

// 수정 전 - 내부 클래스 진입 후 null 체크가 흩어져 있던 구조
public class StreamService {
    public Result process(Param param) {
        InnerHandler handler = new InnerHandler(param);
        return handler.execute(); // null 체크는 execute() 안에서
    }

    private class InnerHandler {
        private final Param param;
        InnerHandler(Param param) { this.param = param; }

        Result execute() {
            if (param == null || param.getId() == null) { // 여기서 뒤늦게 체크
                throw new IllegalArgumentException("invalid param");
            }
            // ...
        }
    }
}

// 수정 후 - 진입 전에 명시적으로 검증
public Result process(Param param) {
    validateParam(param); // 바인딩 직후 검증 분리
    InnerHandler handler = new InnerHandler(param);
    return handler.execute();
}

private void validateParam(Param param) {
    if (param == null || StringUtils.isBlank(param.getId())) {
        throw new InvalidRequestException("요청 파라미터가 올바르지 않습니다.");
    }
}

이렇게 분리하면 execute() 안에서는 비즈니스 로직만 남고, 예외 처리 흐름도 추적하기 쉬워진다.

트랜잭션 범위와 예외 전파 방향

DB를 건드리는 로직에서 항상 고민하는 게 트랜잭션 범위다. 이번에도 MyBatis mapper 호출이 포함된 내부 클래스 메서드에서 @Transactional 범위를 명확히 해야 했다. 특히 스트리밍 환경에서 요청이 겹칠 경우, 트랜잭션이 의도치 않게 겹쳐서 데이터 정합성이 깨질 수 있다.

고민했던 포인트를 정리하면:

  • @Transactional을 서비스 메서드에 걸 것인가, 내부 클래스 메서드에 걸 것인가
  • 내부 클래스 메서드는 Spring 프록시 대상이 아니기 때문에, 직접 @Transactional을 붙여도 동작하지 않음
  • 실제 트랜잭션 경계는 public 서비스 메서드에 두고, 내부 클래스는 그 범위 안에서만 DB 접근하도록 설계
구분 처리 방식 비고
정상 흐름 트랜잭션 커밋 서비스 메서드 반환 시
비즈니스 예외 checked exception → 응답 변환 롤백 없음
시스템 예외 RuntimeException 전파 → 트랜잭션 롤백 공통 예외 핸들러 처리

예외 전파 방향에서는 RuntimeException을 그냥 올려보낼지, 잡아서 응답으로 변환할지를 결정해야 했다. 스트리밍 보안 기능 특성상 비정상 요청에 대해서는 응답 코드와 메시지를 클라이언트에 명확히 전달해야 해서, 내부에서 잡아 응답 변환하는 쪽을 선택했다. 반면 DB 연결 실패 같은 시스템 예외는 공통 핸들러로 올려서 처리하게 했다.

MyBatis SQL XML 쪽에서는 쿼리 자체보다 결과 매핑이 문제였다. 결과가 null로 내려올 때 resultMap 처리가 어떻게 되는지 확인하고, 빈 결과에 대한 처리를 명시적으로 추가했다.

작업 후 남은 생각

로컬 기동 확인 → 개발 DB 반영 → 배포 순서로 처리했는데, 이게 당연한 것 같아도 스트리밍 플랫폼에서는 로컬과 개발 환경 데이터 차이 때문에 놓치는 케이스가 종종 있다. 이번에는 개발 DB에서 null이 실제로 내려오는 케이스를 한 번 더 확인하고 넘어갔다.

운영 로그에 대해서도 한 번 더 챙겼다. 보안 관련 기능이다 보니 어떤 요청이 들어왔고, 어떤 결과를 반환했는지 INFO 레벨로 남겨두는 게 나중에 이슈 추적할 때 유용하다. 다만 보안 입력값을 그대로 로그에 남기면 안 되기 때문에, 마스킹 처리가 필요한 필드는 별도로 정리했다.

이런 류의 작업은 결과물이 눈에 잘 안 보인다. 기능이 새로 생긴 게 아니라, 기존에 동작하던 게 더 안전하고 견고해진 것이기 때문이다. 그래도 이런 수정들이 쌓이지 않으면 운영에서 예외 케이스가 터질 때마다 대응에 쫓기게 된다. 작업 단위가 작을수록 꼼꼼하게 추적하는 게 맞다고 생각해서, 수정한 로직이 기존 흐름에 영향 없는지 관련 코드를 같이 훑는 걸 이번에도 빠뜨리지 않았다.

댓글 0

첫 댓글 달아줘.