이번에 AWS로 스프링 부트 서버를 배포해보면서 CI/CD 플랫폼인 Github Actions를 사용해 보았다.

 

CI/CD란?

- CI: 지속적 통합으로 빌드 -> 테스트 -> 통합의 과정을 자동화해주는 것

- CD: 지속적 배포로 운영 서버에 자동 배포하는 것

 

스프링 부트에는 톰캣이 내장되어 있어서 gradle로 빌드를 하면 패키징된 jar 파일이 생성되고 이 jar 파일을 실행하면 간단하게 서버를 띄울 수 있다.

 

전체적인 흐름을 보면 Github Actions를 통해 다음과 같은 워크플로우를 자동화하는 것이다.

 

"애플리케이션을 gradle로 빌드해서 jar 파일을 생성하고 jar 파일을 AWS 운영 서버에 전달을 해서 배포한다."

 

물론 깃액션이 모든 작업을 처리하는 것은 아니다.

 

먼저 AWS 환경을 구축해야 하는데 처음 배포를 하기 위해 AWS 관련 포스팅을 보면 설정이 조금씩 바뀌어서 헷갈리는 부분이 많다. 그래서  각각 작업에서 어떤 설정이 필요한지 이해하는 것이 좋다.

 

AWS 환경을 구축하기 위한 전체적인 순서는 다음과 같다.

 

1. IAM 사용자 생성

2. 서버를 배포하기 위한 클라우드 컴퓨팅 서비스 EC2 생성

3. 관계형 데이터베이스 서비스 RDS 생성

4. 온라인 스토리지 S3 (Jar 파일을 저장) 생성

5. 자동 배포 서비스 CodeDeploy 생성

 

 

1. IAM 사용자 생성

 

AWS에서 root 계정을 직접 사용하는 것은 보안상 권장하지 않아서 먼저 root 계정으로 로그인을 하고 IAM 사용자를 생성해서 필요한 작업을 한다. IAM 사용자를 생성할 때는 사용자 그룹에 필요한 권한을 추가해서 생성하고 해당 그룹에 사용자를 추가하는 식으로 생성한다.

 

사용자 그룹 생성 시 AdministratorAccess 권한을 주고 해당 그룹에 사용자를 추가했는데 AdministratorAccess는 거의 모든 권한을 가지기 때문에 실제로 사용하는 것은 위험할 것 같다. (사용자 권한에 대해서도 공부를..)

 

2. EC2 인스턴스 생성

 

EC2 메뉴에서 인스턴스 시작을 하면 생성을 할 수 있는데 하나씩 보도록 한다.

 

AMI(Amazon Machine Image)의 경우 서버 환경을 구성하는 것이다. AMI를 만들어두면 추후 다른 인스턴스에서 복제해서 사용할 수 있다. 인스턴스 유형은 CPU, 메모리 등을 정하는 건데 프리티어는 크게 선택할 것이 없다.

 

키페어(key-pair)인스턴스에 접속하기 위한 키(*.pem)를 생성하는 것으로 SSH로 인스턴스에 접속하기 위해 필요하다. 일단 자동 생성을 해둔다.

 

네트워크 설정을 보면 네트워크(vpc-0ad12dxxx), 서브넷, 보안 그룹이 보이는데 먼저 IP에 대해 이해를 해야 한다.

 

IP는 인터넷상에서 고유한 주소를 나타내는 공인 IP와 RFC1918에서 내부 네트워크로 지정된 대역인 사설 IP로 나뉜다.

공인 IP는 네이버, 구글과 같은 도메인의 IP를 나타내고 사설 IP는 공유기를 생각하면 이해하기 쉽다. 공유기에 연결된 노트북, 핸드폰의 와이파이 설정을 보면 사설 IP를 확인할 수 있는데 인터넷망에 접속할 때는 공유기의 NAT 기능을 통해 해당 공유기의 공인 IP로 변환이 된다.

(유튜브 널널한 개발자님의 공유기 관련 영상을 보는 것을 추천..!)

 

 

 

이제 다시 VPC와 서브넷을 알아보자.

 

VPC는 AWS에서 서비스를 개발, 제공하기 위한 가상 사설 네트워크망으로 사설 IP(Private Network)를 활용하여 네트워크 망을 구성하는 서비스를 말한다. VPC는 하나의 리전에 종속되며 서로 독립적(Private Network)이다.

그리고 VPC를 더 작은 범위의 네트워크로 나눈 것서브넷이라 하는데 서브넷은 AZ서비스에 속하며 서브넷 내 EC2, RDS와 같은 리소스들을 위치시킬 수 있다.

 

AWS 사이트에서 VPC를 검색해서 눌러보면 이미 생성되어 있는 VPC를 확인할 수 있는데 ID는 위에서 EC2 인스턴스를 생성할 때 본 것과 같고 IPv4 CIDR은 172.31.0.0/16으로 나와있다. (CIDR은 IP 주소 할당 방법으로 계산 방법은 따로 찾아본다.)

 

 

그리고 서브넷도 눌러보면 위의 VPC ID를 가지는 서브넷이 4개가 있고 IPv4 CIDR은 172.31.xx.x/20으로 되어 있다.

(같은 VPC를 가지는 4개의 서브넷이 있고 IP 또한 같은 범위의 사설 IP 대역에 속하는 것을 볼 수 있다.)

 

위에서 기본으로 등록이 되어 있는 VPC, 서브넷은 서울 리전의 VPC와 서울 리전을 이루고 있는 가용 영역(AZ)의 서브넷을 나타내는 것이다. (EC2 인스턴스를 생성할때 따로 설정하지 않으면 기본적으로 등록이 된다.)

 

(VPC에 대해 자세한 내용은 아래 잘 정리된 블로그를 추천)

 

[AWS] 가장쉽게 VPC 개념잡기

가장쉽게 VPC 알아보기

medium.com

 

Virtual Private Cloud(VPC) 쉽게 이해하기 #1

VPC(Virtual Private Cloud) Amazon Virtual Private Cloud(Amazon VPC)에서는 사용자가 정의한 가상 네트워크로 AWS 리소스를 시작할 수 있습니다. 이 가상 네트워크는 AWS의 확장 가능한 인프라를 사용한다는 이점

aws-hyoh.tistory.com

 

 

 

보안 그룹은 방화벽과 같은 역할을 하는데 EC2 인스턴스 생성 후에 따로 설정을 한다. 마지막으로 스토리지는 프리티어에서 30GB까지 무료로 제공하기 때문에 30GB로 설정하고 인스턴스를 생성한다.

 

EC2 인스턴스가 잘 생성된 것을 확인하고 네트워크 및 보안에서 보안 그룹, 탄력적 IP를 순서대로 설정한다.

 

 

보안 그룹을 누르면 아까 기본으로 생성된 보안 그룹이 보이는데 인바운드 규칙, 아웃바운드 규칙을 추가할 수 있다.

인바운드 규칙은 외부 -> EC2로 허용할 IP 대역을 설정하는 것이고 아웃바운드는 EC2 -> 외부로 반대를 나타낸다.

보안 그룹은 Stateful(상태를 저장)하기 때문에 한번 인바운드를 통과하는 트래픽은 아웃바운드 규칙 적용을 받지 않는다. (반대 경우도)

 

 

인바운드 규칙에 SSH 접속을 위해 내 IP만 허용해서 추가를 하고 Spring Boot 기본 포트 8080을 Anywhere(0.0.0.0/0)로 추가한다.

 

다음으로 탄력적 IP를 추가한다. 위에서 EC2 인스턴스를 생성할 때 네트워크 설정을 보면 퍼블릭 IP 할당이 활성화로 되어있어서 인스턴스를 다시 실행할 때마다 외부에서 접근 가능한 공인 IP가 새로 할당이 되는데 탄력적 IP는 고정 IP를 할당받는 것이다. (탄력적 IP를 생성하고 사용하지 않으면 요금이 나오니 조심)

 

탄력적 IP 주소 할당을 하고 연결만 하면 간단하게 된다. 다시 EC2의 인스턴스로 가보면 퍼블릭 IP가 고정 IP로 할당된 것을 확인할 수 있다.

 

이제 EC2 인스턴스를 생성해서 외부에서 EC2에 접속이 가능하도록 보안 그룹의 인바운드 규칙을 추가하고 탄력적 IP까지 추가했으니 로컬에서 SSH로 접속을 해본다.

 

 

EC2 인스턴스를 체크하고 연결을 누르고 SSH 접속 명령어를 복사하면 된다. (예시는 키페어가 있는 위치에서 명령어를 실행)

 

맥 OS 기준으로 호스트를 등록해서 간단하게 실행하는 방법도 있다.

먼저 키페어(*.pem)를 터미널 ~/.ssh 경로로 이동시킨다.

~/.ssh 경로에 config 파일을 생성하고 아래 내용을 추가한다

(HostName은 퍼블릭 IP 또는 퍼블릭 DNS를, User는 기본 이름인 ubuntu, IdentityFile은 키페어 위치를 입력)

 

저장을 하면 Host명으로 간단하게 접속이 가능해진다.

 

EC2 인스턴스에 ssh로 접속을 했으면 Jar 파일을 실행하기 위해 JDK 설치를 해준다.

sudo apt-get update
sudo apt-get install openjdk-11-jdk

 

배포할 프로젝트를 gradle로 build하는데 Spring Boot 2.5 버전 이상은 build를 하면 jar 파일을 생성하는 bootJar Task와 jar Task가 둘 다 실행이 되어서 jar 파일이 두개가 생성이 된다. 실행 가능한 Jar를 생성하는 bootJar Task만 필요하기 때문에 build.gradle에 jar enabled 옵션을 false로 바꾸도록 한다. (false로 추가하지 않으면 plain.jar 파일도 같이 생성이 된다.)

 

 

로컬에서 SSH 원격으로 Jar 파일을 전송하기 위해 scp(Secure Copy) 명령어로 build된 jar 파일을 EC2 서버로 복사한다.

 

java -jar로 실행하고 테스트 요청을 보내본다.

 

 

 

 

AWS 1편: EC2 생성 후 Spring Boot 띄우기

Overview AWS EC2 인스턴스를 생성하고 Spring Boot 서버를 띄워보는 것까지 진행합니다. 주 목표는 서버를 외부에 제공하는 거라서 따로 배포 시스템을 구축하지 않고 단순히 빌드 파일을 복사해서 수

bcp0109.tistory.com

자바로 WAS를 구현해 보았는데 사실 이번 미션은 HTTP와 WAS에 대해 공부하는 것인데 스프링 MVC 구조에 집중해서 조금 아쉬웠다. 

 

프록시 패턴에 대해 간단 정리

더보기

 처음에는 컨트롤러에서 여러 메서드를 핸들링하려고 하다 보니 컨트롤러에 요청을 받는 public 메서드와 각각의 처리(회원가입, 유저 조회 등)를 하는 private 메서드를 만들어야 했다. 그래서 내 맘대로 프록시라는 클래스를 만들고 Enum으로 매핑을 해주었다.

이렇게 컨트롤러에 메서드를 추가할때마다 프록시와 Enum 클래스를 수정하는 이상한 코드가 완성이 되었다. 내가 원하는 것은 컨트롤러에 메서드만 추가하면 동작이 되는 것이었기 때문에 결국 김영한님의 스프링 고급편 강의로 어설프게 들어본 프록시 패턴과 리플렉션에 대해 공부를 해보았다.

 

프록시 패턴은 접근 제어, 캐싱, 부가 기능(데코레이트) 등의 목적으로 사용이 된다. GOF 디자인 패턴에서는 의도에 따라 프록시 패턴과 데코레이터 패턴으로 나누는데 프록시 패턴은 접근 제어가 목적이고, 데코레이터 패턴은 새로운 기능 추가가 목적이다.

이전에 Redis로 캐싱을 할 때 @Cacheable로 간단하게 캐싱을 했었는데 스프링부트는 애노테이션으로 프록시 AOP 기술을 쉽게 사용할 수 있게 해 준다. 아래 예시는 공식 문서에서 @Transactional을 예시로 들었는데 @Transactional 애노테이션을 사용하면 프록시가 트랜잭션 관련 처리를 해준다. 프록시와 실제 비즈니스 로직이 있는 AccountServiceImpl이 같은 AccountService를 구현하고 있는 것을 볼 수 있다.

 

프록시와 대상은 다형성으로 호출하는 쪽에서는 프록시를 호출한 것인지 실제 대상을 호출한 것인지 몰라야 된다. 

 

인터페이스 대신 부모 상속으로도 가능하지만 결국 프록시 클래스를 계속 직접 만들어야 되는 것은 변하지 않는다. JDK 동적 프록시는 InvocationHandler를 통해 동적으로 프록시를 생성할 수 있지만 인터페이스가 필요하다는 조건이 있고 CGLIB를 사용하면 인터페이스가 없이도 동적 프록시를 생성할 수 있다고 한다.

 

결국 디자인 패턴은 의도가 중요하다고 하는데 아직 디자인 패턴을 깊게 볼 때는 아닌것 같고 현재 나는 요청을 전달하고 싶은 것뿐이라서 @RequestMapping 애노테이션을 만들고 리플렉션으로 요청에 맞는 컨트롤러와 메서드를 찾아서 실행(invoke)해주는 방향으로 전환했다.

 

 

전체적인 흐름은 아래와 같은데 템플릿 엔진은 구현하지 못했다.

 

 

먼저 동시에 여러 요청을 처리하기 위해 ThreadPool을 생성하고 serverSocket에서 accept()를 한다.

accept() 호출 시 메인 스레드는 block 상태가 되고 연결이 되면 생성한 소켓(connect)을 반환해서 스레드풀로 비동기 처리를 한다. 

 

 

정리하면서 알았는데 executorService.shutdown()이 호출되면 새로운 작업은 실행하지 않고 현재 실행중인 작업만 처리한다.

하지만 실행중인 작업이 종료가 될 때까지 blocking 하지 않기 때문에 실행 중인 작업까지 처리하기 위해서는 awaitTermination(), shutdownNow()를 활용해서 종료해야 한다.

 

 

HttpHandler는 Http 요청, 응답을 처리하는 핸들러로 request를 파싱해서 handle()로 보낸 뒤 response가 담긴 결과를 sendResponse()로 보낸다.

 

handle()은 RequestUrl의 확장자를 통해 정적 리소스 요청인지, 동적 리소스 요청인지 체크한다. 동적 요청의 경우 싱글톤으로 생성한 DispatcherServlet을 가져와서 doDispatch()를 실행한다. HttpHandler는 요청당 생성이 되고 DispatcherServlet을 관리하는 별도의 컨테이너가 없어서 싱글톤으로 생성을 했다.

 

doDispatch()는 ControllerAdapter에서 handle()을 통해 컨트롤러에게 요청을 전달해서 처리하고 ModelAndView를 반환받는다.

(예외처리나 리다이렉트 처리 부분은 필터를 적용하면 좋을 것 같은데 구현을 못했다.)

 

ControllerAdapter는 Controller 클래스들의 @RequestMapping 정보를 읽어서 요청을 처리할 수 있는 Controller를 찾는 역할을 한다. findAllMappingUrl()은 컨트롤러의 @RequestMapping 매핑 정보를 초기화하기 위해 사용한다

 

다시 ControllerAdapter의 handle() 메서드를 보면

 

1. findController: 요청을 처리할 수 있는 컨트롤러를 찾고

2. findControllerMethod: 해당 컨트롤러의 메서드를 찾는다.

3. ArgumentResolver를 통해 메서드를 실행하기 위해 필요한 파라미터를 resolve 한다.

4. 메서드 실행에 필요한 arguments를 만들어서 메서드를 실행(invoke)한다.

5. 메서드의 반환값으로 ReturnValueHandler를 통해 ModelAndView를 생성한다. (ViewName, Json Convert 등 처리)

 

MethodArgumentResolver는 인터페이스로, Argument 타입에 따라 나뉘는데 ObjectArgumentResolver는 ObjectBinder를 사용해서 QueryParameter를 객체로 바인딩해 준다.

ObjectBinder는 기본 생성자와 setter를 필요로 한다. setter를 호출하기 위해 setName, setAge와 같은 setter 메서드명을 직접 만들어야 하는데 컴파일 에러가 뜨는 것도 아니라서 위험하다.

MessageBody 처리는 MessageConverter를 사용하는데 원래는 ObjectBinder라는 클래스가 없었지만 여기서도 ObjectBinder 기능이 필요하기 때문에 따로 클래스로 만들게 되었다. (form-data만 파싱만 구현했다.)

 

다시 DispatcherServlet의 doDispatch()로 돌아와서 ViewResolver를 통해 View를 찾고 렌더링을 하면 HttpHandler에서 Response 응답을 스트림으로 보낸다. 맨처음 말한것처럼 HTTP 헤더를 제대로 처리하지 못해서 아쉽다.

 

결과적으로 Controller에서는 어느 정도 의도한 대로 동작을 할 수 있게 되었다. @Valid로 컨트롤러 파라미터의 객체 생성 시 validation을 하는 경우에도 ArgumentResolver가 동작해서 BindingResult에 담아주는데 간단하게나마 구현을 해보니 어떤 식으로 동작이 되는지 이해할 수 있었다. 여전히 새로운 컨트롤러를 생성하는 로직을 추가해야 되는데 이 부분은 DI 컨테이너를 구현해야 될 것 같다.

 

 

 

[참고]

김영한님 스프링 고급편

 

[Spring] @Valid와 @Validated를 이용한 유효성 검증의 동작 원리 및 사용법 예시 - (1/2)

Spring으로 개발을 하다 보면 DTO 또는 객체를 검증해야 하는 경우가 있습니다. 이를 별도의 검증 클래스로 만들어 사용할 수 있지만 간단한 검증의 경우에는 JSR 표준을 이용해 간결하게 처리할 수

mangkyu.tistory.com

 

Transactions, Caching and AOP: understanding proxy usage in Spring

In the Spring framework, many technical features rely on proxy usage. We are going to go in depth on this topic using three examples: Transactions, Caching and Java Configuration. All the code samples shown in this blog entry are available on my github acc

spring.io

 

[Java] ExecutorService.shutdown(Now) & awaitTermination

헷갈리는 ExecutorService 의 종료와 관련된 메소드를 테스트해봤다.

velog.io

 

 

이번에 코드스쿼드 미션으로 웹서버를 구현해 보면서 공부한 내용을 정리해 보았다.

 

[기본으로 주어진 예제 코드]

try (ServerSocket listenSocket = new ServerSocket(port)) {
    Socket connection;
    
    while ((connection = listenSocket.accept()) != null) {
        Thread thread = new Thread(new RequestHandler(connection));
        thread.start();
    }
}

 

1. 소켓(Socket)과 포트(Port)


유튜브 널널한 개발자님

위 그림에서 TCP와 Process 사이 노란 원이 소켓을 표현한 건데 소켓은 유저 모드 애플리케이션에서 커널 영역에 접근하기 위한 통로 역할을 한다.

소켓의 본질은 파일(File)이라고 하는데 여기서 파일은 운영체제 커널 영역에 구현되어 있는 요소들에 대한 추상화된 인터페이스로 이때 프로토콜 요소(여기서는 TCP/IP 프로토콜)에 대해서 추상화를 한 경우 소켓이라고 부른다.

OS에서는 읽고 쓰는 것을 파일로 추상화해서 범용적인 의미로 사용하기 때문에 일반적으로 아는 파일, 디렉토리, 심볼릭 링크(바로가기) 소켓 등 많이 것들이 파일에 포함된다. 처음에는 소켓이 왜 파일인지 의아했는데 결국 소켓도 스트림으로 읽고 쓰는 것이라고 생각하니 조금 이해가 되었다.

 

김영한님 HTTP 강의

소켓의 스트림을 통해 데이터 <-> TCP(세그먼트) <-> IP(패킷) <-> Frame으로 전달이 되는데 여기서 TCP의 주요 특징인 포트가 나온다. 기존에 IP 패킷만 전달할 때는 신뢰성, 순서 보장 등의 문제도 있었지만 한 IP 내에서 여러 애플리케이션이 실행중일 때 어디로 전달이 되어야 하는지를 모르는 문제가 있었다.

 

 

그래서 프로세스를 식별하기 위한 포트 정보를 같이 전달하게 되는데 포트 번호는 16bit(2^16) 0 ~ 65535의 범위를 가지며 일반적으로 1~1023는 용도가 정해진 포트 번호들이 있고, 서버 소켓의 포트로는 1024 ~ 49151 범위로 사용하는 것 같다. 동적 포트는 보통 브라우저에서 클라이언트 쪽에 할당하는 포트 번호이다.

 

- Well Known 포트 : 0부터 1023 (root 권한으로 바인드)

- Registered 포트 : 1024부터 49151 (일반유저 권한으로 바인드)

- Dynamic/Private 포트 : 49152부터 65535 (클라이언트와 서버 간 통신 시 클라이언트가 사용하는 포트)

 

 

 

2. ServerSocket, Socket


 이제 예제 코드에서 사용하는 소켓을 보면 ServerSocket, Socket이 있다.

 

먼저 ServerSocket을 보면 생성자 파라미터로 포트 번호를 전달하고 있는데 해당 포트 번호로 바인딩을 시도하고 정상적으로 바인딩이 되면 서버 소켓은 LISTEN 상태가 된다.

sudo lsof -PiTCP -sTCP:LISTEN

LISTEN 상태에서 서버 소켓은 클라이언트의 연결 요청을 수신하며 TCP/IP 연결을 하고 연결이 되면 연결 정보를 내부적으로 큐에 관리한다. accept()를 호출하면 큐에 있는 연결 정보를 꺼내거나 없을 경우 현재 스레드에서 block 상태로 대기한다. (non-blocking 방식 socket도 있다고 한다.)

 

ServerSocket의 생성자를 보면 backlog로 큐의 사이즈를 설정할 수 있는데 기본 50으로 설정이 된다.

연결 요청을 관리하는 큐는 2개가 있는데 3 Way-Handshake 연결이 된 후 커넥션을 Accept Queue로 이동시키고 서버 소켓에서 accept()로 꺼낼 수 있다. 서버 소켓에서 설정한 backlog가 Accept Queue의 사이즈가 된다. (스프링 부트에서 톰켓의 backlog 값도 설정할 수 있다고 한다..!)

 

다시 예제 코드를 보면 accept()를 통해 Socket 인스턴스가 반환이 되는데 이 소켓은 클라이언트와 연결된 소켓으로 클라이언트쪽 소켓의 포트 정보를 가지고 있으며 Stream을 통해 데이터를 주고받을 수 있다. 즉, 서버쪽 포트를 바인딩하고 클라이언트 연결을 수신하는 ServerSocket클라이언트 연결이 accept() 될 때마다 생성되는 Socket이 있는 것이다.

 

accept()로 생성된 소켓에서 양쪽의 포트 정보를 확인할 수 있다.

 

예제에서는 8080번 포트를 사용했는데 "http://localhost:8080"처럼 뒤에 포트 번호를 붙여주고 전송을 하면 된다. RFC에 기본 포트로 지정이 되어 있는 80(HTTP) 443(HTTPS)로 포트 바인딩을 하면 scheme(HTTP, HTTPS)에 따라 포트 번호 생략이 가능하다.

 

 

3. Thread


예제 코드를 보면 Thread 인스턴스를 생성하고 thread.run() 또는 thread.start()를 호출해서 Runnable 타겟을 실행할 수 있는데 run()의 경우는 스레드가 생성되지 않고 현재 스레드에서 실행이 된다. 반면 start()는 스레드를 생성하고 해당 스레드에서 작업을 실행한다.

무엇을 사용하든 스레드 자체는 new Thread()를 할 때 생성이 되는 줄 알았는데 수업을 듣고 잘못 알고 있는 것 같아서 테스트를 해봤다.

Thread thread = new Thread(Runnable target);

// start()? run()?
thread.start();
thread.run();

 

 

먼저 thread.run()의 경우 blocking이 되고 Runnable의 동작이 시작해서 끝난 뒤에 CALL 메세지가 출력이 된다.

 

thread.start()로 변경한 결과 Runnable의 동작을 기다리지 않고 CALL 메시지가 먼저 출력이 된다.

사실 출력 내용만 봐도 run()은 main 스레드에서 실행을 하고 있고 start()는 Thread-n에서 실행을 하고 있다.

 

main 스레드를 sleep 시키고 thread.start()를 했을 때 스레드의 상태를 확인해 보았다.

반면 run()으로 테스트를 했을 때는 메인 스레드가 sleep이 풀릴 때까지 기다렸다가 실행이 된다.

 

new Thread()를 하면 힙 영역에 인스턴스가 생성되는 것뿐이고(이때 thread 상태는 NEW) 실제 스레드 공간이 할당되는 것은 start()를 호출할 때이다.

 

 

추가로 스레드 덤프에 대해 알게 되었는데 멀티스레드 환경에서 발생할 수 있는 스레드 간 경합, 데드락 등 여러 문제를 분석할 수 있다. 스레드가 제대로 반납이 되지 않을 때 스레드 덤프를 확인해 보면 좋을 것 같다.

현재 내 코드에서는 스레드풀을 사용하기 때문에 WAITING 상태로 대기하는 것을 볼 수 있었다.

 

 

[참고]

 

널널한 개발자님 유튜브, 김영한님 HTTP 강의

 

 

Java client socket returned by ServerSocket.accept()

This is more of a general socket question. In Java, if I have a ServerSocket bound to a specific port, say 4444, I understand that it's listening for connection requests. The accept() method blocks

stackoverflow.com

 

Linux man : listen - 소켓의 연결을 위한 대기열을 만든다.

서버측 프로그램은 socket(2)함수를 이용해서 클라이언트(:12)의 연결을 받아들일 듣기소켓을 만들게 된다. 클라이언트의 연결은 듣기소켓을 통해서 이루어지는데 클라이언트는 connect(2)를 호출해

www.joinc.co.kr

 

스프링 부트에서 socket backlog 모니터링과 튜닝법

 

www.manty.co.kr

 

http의 기본 포트가 80, https의 기본 포트가 443인 이유는 무엇일까?

80은 처음부터 지정, 443은 나중에 요청을 받아 빈 공간으로 순서대로 배정

johngrib.github.io

 

리눅스보안기본#1 : : 포트(port)의 개념과 포트 제어 > 강좌 | 클라우드포털

포트(port)의 개념과 포트 제어 클라이언트/서버 환경에서 시스템의 접속은 포트와 포트간의 통신이라고 할 정도로 포트의 의미는 매우 중요하다. 당연히 시스템 내에서 작동하고 있는 서비스를

www.linux.co.kr

 

개발자를 위한 레시피

Recipes for Developer.

recipes4dev.tistory.com

 

Java client socket returned by ServerSocket.accept()

This is more of a general socket question. In Java, if I have a ServerSocket bound to a specific port, say 4444, I understand that it's listening for connection requests. The accept() method blocks

stackoverflow.com

 

Thread Dump 분석

Thread Dump 생성 방법Unix : kill -3 [PID]window : Ctrl + Break공통 : jstack [PID]반드시 생성시 3~5회 연속으로 생성하여 문제 상황에 대한 변화 과정을 확인 Thread Dump 정보tid (Java-Level Thread ID )를 이용하여 정보

ijbgo.tistory.com

 

[JAVA] - Thread.start()와 Thread.run()의 차이

JAVA로 Thread 관련 프로그래밍을 학습하다보면 start() 메서드와 run() 메서드를 보게되는데 두 메서드를 실행하게되면 Thread의 run() 메서드를 실행하게 된다. 다만 이 두 메서드의 동작방식을 제대로

kim-jong-hyun.tistory.com

 

[Java] JVM Thread Dump 분석하기

java-study에서 스터디를 진행하고 있습니다. JVM Thread Dump 분석하기 스레드 덤프가 필요한 이유 웹 서버에서는 많은 수의 동시 사용자를 처리하기 위해 수십 ~ 수백 개정도의 스레드를 사용한다. 두

steady-coding.tistory.com

https://d2.naver.com/helloworld/10963

 

게시판 만들기를 하면서 기계처럼 붙이고 보는 @Transactional을 왜 사용하는지 머릿속에 정리가 안 돼서 다시 강의를 보면서 정리를 해봤다.

 

데이터베이스를 사용하는 중요한 이유는 하나는 트랜잭션이라는 개념을 지원하기 때문인데 트랜잭션이란 하나의 논리적인 작업 단위로 계좌이체로 예를 들면 돈을 보내고 받는 과정이 하나의 작업이 되는 것이다. 작업이 성공적으로 끝나면 커밋(commit), 중간에 실패하면 되돌리는 롤백을 한다.

 

트랜잭션은 ACID를 보장해야 하는데 이 중에서 격리성(Isolation)은 동시성의 정도를 나타내는데 동시성을 보장하는 것은 성능과의 trade-off가 있어서 격리 수준이 나뉘게 된다.

 

 

 

[DB 접근 기술1] 트랜젝션(Transaction) 기초

트랜젝션 데이터를 저장할 때 단순 파일이 아닌 데이터베이스에 저장하는 가장 큰 이유는 데이터베이스가 트랜젝션이라는 개념을 지원하기 때문이다. 트랜젝션은 데이터베이스에서 하나의 거

treecode.tistory.com

 

[Real MySQL] MySQL의 격리 수준

트랜잭션의 격리 수준이란 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다. 격리 수준 READ U

treecode.tistory.com

 

트랜잭션을 사용하기 위해서는 auto commit을 false로 설정하면 되는데 일반적으로는 디폴트 값이 true로 되어 있다. 이렇게 되면 매번  쿼리마다 자동으로 커밋이 되다 보니 하나의 작업 단위를 묶을 수가 없어서 auto commit 옵션을 false로 주고 개발자가 직접 commit, rollback을 해야 된다.

 

커넥션 연결을 하게 되면 데이터베이스 내부에 세션이 생성되고 세션을 통해서 트랜잭션을 시작하고 SQL을 실행, 커밋, 롤백, 트랜잭션 종료와 같은 작업이 이루어진다.

하나의 논리적인 작업 단위의 기준은 비즈니스 로직으로 서비스 계층에서 시작을 해야 하는데 이렇게 되면 서비스 클래스에서 커넥션을 꺼내기 위해 DataSource를 가지고 트랜잭션 시작, commit, rollback, 트랜잭션 종료 등의 처리를 하는 코드가 들어가게 된다.

 

public void accountTransfer(String fromId, String toId, int money) throws
  SQLException {
	Connection con = dataSource.getConnection();
	try {
        con.setAutoCommit(false); //트랜잭션 시작 //비즈니스 로직
        bizLogic(con, fromId, toId, money); con.commit(); //성공시 커밋
    } catch (Exception e) { con.rollback(); //실패시 롤백
        throw new IllegalStateException(e);
    } finally {
        release(con);
    }
}

위의 예시에서 bizLogic()에서는 트랜잭션을 유지하기 위해 동일 커넥션을 사용해야 되고 repository를 호출할 때마다 커넥션을 파라미터로 전달해줘야 한다. 그리고 SQLException 같이 JDBC 전용 예외 같은 구체화에 의존성이 생기는 등 JdbcTempalte, JPA .. 데이터 접근 기술의 변경에 영향을 받게 된다.

 

스프링은 이러한 문제를 해결하기 위해 트랜잭션 추상화 인터페이스 TransactionManager를 제공하는데 트랜잭션 추상화, 커넥션 동기화 역할을 대신해 준다. 

 

 

커넥션은 트랜잭션 동기화 매니저가 스레드별 저장소인 스레드 로컬을 통해 동기화를 해서 파라미터로 전달하지 않아도 유지할 수 있다. 스레드 로컬은 사용 후 정리하는 것이 매우 중요하기 때문에 (+ 커넥션 반납 등) 개발자가 직접 하기보다는 제공되는 기술을 사용하는 것이 안전하다.

 

트랜잭션 매니저를 사용해도 커밋, 롤백 try-catch를 하는 코드가 남아있는데 스프링 AOP를 활용한 @Transactional을 사용하면 깔끔하게 처리가 가능해진다.

 

@Transactional을 정확히 이해하려면 스프링 AOP를 학습해야 되는데 일단은 왜 써야 하는지 정도만 알고 넘어갔다.

@Transactional을 클래스에 메서드에 사용하면 트랜잭션 AOP는 프록시를 만들어 스프링 컨테이너에 등록하는데 이 프록시가 트랜잭션을 관리하는 로직을 대신 처리해 주게 된다. 

 

한 가지 주의점은 외부에서 요청을 하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고, 실제 메서드를 호출해 주는데 만약 내부에서 메서드를 호출하는 경우 프록시를 거치지 않아서 트랜잭션이 적용되지 않는 문제가 생긴다.

이전에는 클래스에 @Transactional을 readOnly 설정으로 두고 데이터 삽입, 수정, 삭제 메서드만 따로 @Transacional을 선언해서 사용을 해서 그런지 이런 위험 요소를 인식하지 못했다.

 

클래스에 @Transactional을 선언하는 경우 트랜잭션이 의도하지 않은 곳까지 과도하게 적용이 돼서 메서드 단위로 적용을 한다. 트랜잭션은 주로 비즈니스 로직의 시작점에 걸기 때문에 외부에서 열어둔 곳을 시작점으로 해서 클래스에 @Transactional 애노테이션을 선언해도 public 메서드에만 적용이 된다.

 

 

추가적으로 스프링 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백하는데 RuntimeException , Error와 그 하위 예외가 발생하면 롤백, 체크 예외와 그 하위 예외가 발생하면 커밋을 한다. (@Transactional에서 rollbackFor 옵션으로 변경 가능)

체크 예외의 경우 비즈니스적인 예외 상황에서 주로 사용한다고 하는데 예를 들면 회원의 주문 Order에서 잔고 부족의 경우 일단 커밋을 하고 추가 결제를 하도록 할 수도 있다. (생각해 보니 체크 예외는 사용한 적이 없는 거 같은데 중요한 비즈니스 예외 상황에서 사용하면 좋을 것 같다.)

 

간단 정리

@Transactional 사용하는 이유

 

- 애플리케이션에서 트랜잭션 관리

- 비즈니스 로직과 관련 없는 중복 코드 제거

- 예외 전파, 관리

 

JdbcTemplate은 템플릿 콜백 패턴을 사용해서 JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해 준다.

 

- 커넥션 획득, 종료, statement, resultset 종료

- 트랜잭션을 다루기 위한 커넥션 동기화

- 예외 발생 시 스프링 예외로 변환

 

JdbcTemplate에서 tryForStream() 메서드를 사용하면 예외적으로 직접 리소스 종료를 해줘야 하는데 그렇지 않을 경우 커넥션 반납이 되지 않는 문제가 생긴다. 같이 공부하는 분이 커넥션 반납이 되지 않는다고 물어보셨을 때 @Transactional 역할을 잘 알고 있었으면 이 부분을 먼저 확인해 봤을 거 같은데 금붕어처럼 다 까먹고 말똥말똥 쳐다보고 있었다. @Transactional을 사용하면 트랜잭션 기능 외에도 DB 접근 기술마다 커넥션이 종료되지 않는 상황에서 안전하게 처리를 해주는 이점도 있는 것 같다.

 

 

[참고]

인프런 김영한님 DB 접근 기술 1,2편

+ Recent posts