포스트

2024-07-09-TIL

2024-07-09-TIL

Today I Learned

Forward vs Redirect

둘 다 요청을 다른 곳으로 넘기는 방법이지만 누가 이동하느냐가 다르다.

  • Forward: 서버 내부에서 요청을 다른 리소스(서블릿, JSP, 컨트롤러)로 넘긴다. 서블릿에서는 RequestDispatcher.forward()를 쓴다. 클라이언트는 이동을 모르므로 브라우저 주소창의 URL이 바뀌지 않고, 같은 요청 객체를 공유하므로 request attribute가 그대로 전달된다. 요청-응답이 한 번이다.
  • Redirect: 서버가 3xx 상태 코드와 Location 헤더를 응답하고, 브라우저가 그 주소로 새 요청을 보낸다. 서블릿에서는 HttpServletResponse.sendRedirect()를 쓴다. URL이 바뀌고 요청이 두 번 일어나며, 첫 요청의 데이터는 새 요청에 남지 않는다.

주문 저장처럼 상태를 바꾸는 POST 요청 뒤에는 결과 페이지로 redirect하는 것이 일반적이다(PRG, Post/Redirect/Get). forward로 처리하면 사용자가 새로고침할 때 같은 POST가 다시 전송되기 때문이다. 단순 조회 흐름에서 내부적으로 화면만 바꿀 때는 forward가 쓰인다.

ALB 리스너 규칙에도 redirect 동작이 있다. 리스너 규칙은 우선순위, 조건, 동작으로 구성되고, 동작으로 대상 그룹으로 보내는 forward와 다른 URL로 보내는 redirect(301 또는 302)를 지정할 수 있다. HTTP 80 리스너에서 HTTPS 443으로 redirect하는 설정이 대표적인 예다.


504 Gateway Timeout Error

504는 게이트웨이나 프록시 역할을 하는 서버가 뒤쪽(upstream) 서버로부터 제때 응답을 받지 못했을 때 돌려주는 상태 코드다. 502 Bad Gateway가 upstream에서 잘못된 응답을 받았거나 연결 자체가 실패한 경우라면, 504는 연결은 됐지만 응답이 시간 안에 오지 않은 경우다.

Nginx 앞에 Gunicorn 같은 애플리케이션 서버를 두는 구성에서는 Nginx의 proxy_read_timeout(기본 60초) 안에 애플리케이션이 응답하지 못하면 504가 난다. 해결은 두 방향이다.

  • 원인 제거: 느린 쿼리, 외부 API 대기, 무거운 동기 작업을 찾아 줄이거나, 오래 걸리는 작업은 비동기 작업으로 분리하고 상태 조회 API를 둔다.
  • 타임아웃 조정: 정말 오래 걸리는 요청이라면 프록시와 애플리케이션 서버의 타임아웃을 함께 늘린다. 클라이언트, 로드 밸런서, Nginx, 애플리케이션 서버로 이어지는 경로에서 바깥쪽 타임아웃이 안쪽보다 짧으면 바깥쪽에서 먼저 끊긴다는 점을 고려해야 한다.
1
2
3
4
location / {
    proxy_pass http://app;
    proxy_read_timeout 120s;
}

Method Inlining in Java

메서드 인라이닝은 호출되는 메서드의 본문을 호출 지점에 직접 펼쳐 넣는 최적화다. 메서드 호출 자체의 비용(스택 프레임 생성, 점프)을 없애는 것보다 더 큰 효과는, 펼쳐진 코드를 한 덩어리로 보고 상수 전파나 불필요한 코드 제거 같은 후속 최적화를 할 수 있게 된다는 점이다.

Java에서는 javac가 아니라 실행 중의 JIT 컴파일러(HotSpot의 C1, C2)가 인라이닝을 결정한다. 자주 호출되는 메서드와 바이트코드 크기가 작은 메서드가 대상이 된다. 그래서 getter나 짧은 위임 메서드를 여러 개로 나눠도 실행 성능에서 손해를 거의 보지 않는다. 인터페이스나 오버라이드된 메서드도 런타임에 실제 구현이 하나뿐이라고 판단되면 인라이닝될 수 있고, 이후 다른 구현이 로드되면 역최적화된다.


@Transactional and Self-invocation

Spring의 @Transactional은 기본적으로 프록시로 동작한다. 외부에서 빈의 메서드를 호출하면 프록시가 트랜잭션을 시작하고 실제 객체의 메서드를 호출한다. 그런데 같은 클래스 안에서 this.otherMethod()처럼 자기 메서드를 호출하면 프록시를 거치지 않으므로, otherMethod에 붙은 @Transactional은 무시된다. Spring 문서도 프록시 모드에서는 프록시를 통해 들어온 외부 호출만 가로채며 self-invocation은 실제 트랜잭션으로 이어지지 않는다고 명시한다.

1
2
3
4
5
6
7
8
9
@Service
public class OrderService {
    public void placeAll(List<Order> orders) {
        orders.forEach(this::place); // 프록시를 거치지 않음
    }

    @Transactional
    public void place(Order order) { ... }
}

해결 방법은 트랜잭션이 필요한 메서드를 별도 빈으로 분리해 외부 호출이 되게 하는 것이 가장 단순하다. 바이트코드를 직접 위빙하는 AspectJ 모드를 쓰면 self-invocation도 트랜잭션이 적용되지만 설정이 복잡해진다.


How to Check the MySQL Connector Version

Spring Boot 프로젝트에서 MySQL 드라이버 버전은 빌드 도구로 확인한다. Spring 가이드의 예제처럼 com.mysql:mysql-connector-j를 버전 없이 선언하면 Spring Boot의 의존성 관리가 정한 버전이 들어오므로, ./gradlew dependencies나 mvn dependency:tree로 실제 해석된 버전을 봐야 한다.

Aurora MySQL을 쓴다면 AWS JDBC Driver for MySQL(aws-mysql-jdbc)도 선택지였다. MySQL Connector/J를 기반으로 만든 드라이버로, Aurora 클러스터 토폴로지를 캐시해 장애 조치(failover) 시 DNS 갱신을 기다리지 않고 새 primary로 빠르게 다시 연결한다. 다만 README에 따르면 이 프로젝트는 2024년 7월 25일 지원 종료가 예정되어 있고, 새 기능은 AWS Advanced JDBC Wrapper에서 개발되므로 그쪽으로 옮기는 것이 권장된다.


Software Versioning

소프트웨어 버전 표기 방식 중 가장 널리 쓰이는 것은 Semantic Versioning이다. MAJOR.MINOR.PATCH 형식으로, 하위 호환되지 않는 API 변경은 MAJOR, 하위 호환되는 기능 추가는 MINOR, 하위 호환되는 버그 수정은 PATCH를 올린다. 이 규칙 덕분에 의존성을 선언할 때 “MAJOR가 같으면 안전하게 올릴 수 있다”는 판단을 할 수 있다.

이 외에도 날짜를 버전으로 쓰는 Calendar Versioning(예: Ubuntu의 24.04), 단순 증가 번호, 빌드 번호를 덧붙이는 방식이 있다. 어떤 방식을 고르든 팀 안에서 규칙을 하나로 정하고, 버전마다 변경 내역(changelog)과 Git 태그를 함께 남겨 두면 배포된 버전과 코드를 쉽게 대응시킬 수 있다.

참고한 자료외부 출처 15

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다