Http와 Https는 모두 OSI(Open System Interconnection) 7계층에서 말하는 7-응용계층(application)의 프로토콜이다.

 

여기서 Application 계층은 '일반적인 응용 서비스를 수행하는 역할을 담당하는 계층'으로 여러 프로토콜에 대하여 사용자 인터페이스를 제공한다고 한다. 

대표적인 프로토콜로는 text기반의 Http(hypertext transfer protocol), FTP(File Transfer Protocol), SMTP(Small Mail Transfer Protocol) 등이 있다. 각각 데이터를 텍스트 기반으로 주고받는 프로토콜, 서버와 클라이언트 사이에 파일을 주고받기위한 프로토콜, 이메일을 보내기 위해 이용되는 프로토콜을 말한다.

 

 

https://coderepublics.com/blog/internet/what-is-osi-model-7-layers-explained/#google_vignette

 

What is OSI Model in Computer Network | 7 Layers Explained - CodeRepublics

OSI 7 Layers describes how computers travel over a network from a physical medium to a software application. Networking devices like routers,

coderepublics.com

https://coderepublics.com/blog/internet/what-is-osi-model-7-layers-explained/ 사진출처

 

HTTP와 HTTPS란

Http는 Hypertext Transfer Protocol로 클라이언트와 서버간에 데이터, 문서, 이미지 등을 주고받는 텍스트기반의 응용계층 프로토콜을 말한다.

 

내가 하나의 API를 실행했을때 Request Header와 Response Header는 모두 사람이 읽을 수 있는 평문구조의 텍스트이다. 

이처럼 텍스트 기반 + 정해진 규약에 따라 요청하고 응답하는 형식을 띄고 있다.

단, 순수 http는 암호화되지 않았기 때문에, 네트워크 7계층을 지나면서 요청/응답을 엿볼 수도 있고, 변환할 수 도 있다는 위험성이 있다. 특히 3-5계층을 지나면서 패킷을 캡쳐하거나 중간에 끼어들면 MITM 공격을 할 수도 있다.

 

그래서 나온 것이 Http 에 SSL(Secure Socket Layer: 지금은 안쓴다고함) 혹은 TLS(Transport Layer Security:전송 계층 보안) 가 합쳐진 HTTPS!

 

HTTPS는 텍스트 기반의 Request, Response를 모두 암호화한다. 즉, HTTP + Secure

위의 OSI 7계층 중, 6계층-표현계층 (Presentation) 계층에서는 애플리케이션이 다루는 정보를 통신에 알맞은 형태로 만들거나, 하위에서 온 데이터를 사용자가 이해할 수 있는 형태로 만든다. 이때 암호화가 이 계층에서 진행된다.

 

Https의 인증서와 암호화

나의 경우에는 Let's Encrypt를 사용해서 공개키가 포함된 인증서를 받아서 톰캣 서버에 설정해두었다.

클라이언트가 최초에 서버로 접속할 때 서버가 인증서를 보내면, 클라이언트는 이를 검증해서 대칭키를 생성한다.

서버는 

 

작성중...

 

Spring 프로젝트 배포가 아닌 React 배포이므로 이 카테고리에 적어둔다

 

먼저 리액트에서 npm run build를 사용해 빌드를 만든다

 

 

그리고 빌드 파일이 만들어진 해당 폴더에 가서 git bash를 열고 아래 코드를 입력한다

빌드파일 내용을 EC2의 frontend 폴더로 이전해주는 코드이다.

scp -i "/c/Users/folder/.ssh/forplanbee.pem" -r * ubuntu@***.***.***.site:/home/ubuntu/tomcat/webapps/frontend/

//처음 경로는 .pem 키파일이 있는 폴더 // 그다음은 우분투 도메인 혹은 아이피 그리고 내부에 fe를 받을 폴더
//fe파일을 받을 폴더는 미리 만들어둔 상태이다

 

continue connecting 물음에서 yes를 입력하면 아래와 같이 build 폴더내의 파일들이 EC2로 업로드 된다.

 

그리고 EC2에서 nginx를 재시작한다

sudo systemctl restart nginx

 

 

'React' 카테고리의 다른 글

[React] 실행 순서에 대한 궁금증, useEffect  (0) 2025.03.13
[React] 리액트 기초 실습  (1) 2025.03.11
[React] 수업 필기내용  (0) 2025.03.08

스프링 버전관리의 까다로움을 느낄 수 있었던 redis 설정하기.

 

아래 두 분의 블로그를 많이 참고하여 진행하였다.

거의 기초 틀은 이 두분의 블로그를 복붙해서 진행함

https://se-dobby.tistory.com/70

 

Spring Web MVC에서 Spring session redis 적용

기존에 Velog에서 포스팅을 했었는데, 블로그를 옮기기도 했고 최근에 다른 프로젝트에서 이 과정이 필요해 기존 게시글을 조금 다듬어서 포스팅하게 되었다.  Spring Web MVC에서 Spring session redi

se-dobby.tistory.com

https://velog.io/@1876060677/Spring-API%EC%97%90-Spring-session-redis-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

 

Spring API에 Spring session redis 적용하기

Spring session을 사용한 이유는 로드 밸런싱을 위해서 여러 개의 백엔드 컨테이너를 띄웠는데, 각각의 컨테이너 별로 세션을 가지고 있으면, 로드 밸런싱을 최초에 접속한 컨테이너로 사용자가 계

velog.io

 

 

1. maven repository에 의존성 설정

	<!-- spring-session-data-redis -->
		<dependency>
    		<groupId>org.springframework.session</groupId>
    		<artifactId>spring-session-data-redis</artifactId>
    		<version>2.2.1.RELEASE</version> 
		</dependency>
		
		<!-- spring-data-redis -->
		<dependency>
    		<groupId>org.springframework.data</groupId>
    		<artifactId>spring-data-redis</artifactId>
    		<version>2.2.1.RELEASE</version>
		</dependency>
		

		<!-- lettuce-core -->
		<dependency>
    		<groupId>io.lettuce</groupId>
    		<artifactId>lettuce-core</artifactId>
    		<version>5.2.1.RELEASE</version>
		</dependency>

내가 쓰는 5.2.1.RELEASE 버전에 맞는거 해달라고 챗지피티한테 물어봐서 위와 같이 설정하였다.

 

2. RedisConfig Class 생성

package com.pj.planbee.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisConfig {
	 	@Bean
	    public LettuceConnectionFactory connectionFactory() {
	 		RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
	 				config.setHostName("localhost");
	 				config.setPort(6379);
	 				config.setPassword(RedisPassword.of("1234"));
	 				return new LettuceConnectionFactory(config);
	    }
	 	
	 	@Bean
	    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory ) {
	        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
	        redisTemplate.setKeySerializer(new StringRedisSerializer());
	        redisTemplate.setValueSerializer(new StringRedisSerializer());
	        redisTemplate.setConnectionFactory(redisConnectionFactory);
	        return redisTemplate;
	    }
	 	
	 	@Bean
	    public CookieSerializer cookieSerializer() {
	        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();

	        // 쿠키 설정
	        cookieSerializer.setCookiePath("/");
	        cookieSerializer.setCookieName("SESSION");
	        cookieSerializer.setUseSecureCookie(true); //https에서는 true로
	        cookieSerializer.setUseHttpOnlyCookie(true);
	        cookieSerializer.setSameSite("None"); //실제 https에서는 None
	        

	        return cookieSerializer;
	    }
}

 

3. Filter 설정 - root-context.xml에 설정

 <!-- 1. RedisConnectionFactory -->
	<bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
    	<property name="hostName" value="localhost" />
    	<property name="port" value="6379" />
    	<property name="password" value="1234" />
</bean>

<!-- 2. RedisTemplate 등록 -->
	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
    	<property name="connectionFactory" ref="redisConnectionFactory" />
	</bean>

<!-- 3. Redis 기반 세션 저장소 -->
	<bean id="sessionRepository" class="org.springframework.session.data.redis.RedisOperationsSessionRepository">
    	<constructor-arg ref="redisTemplate" />
	</bean>

<!-- 4. 필터 Bean 등록 -->
	<bean id="springSessionRepositoryFilter" class="org.springframework.session.web.http.SessionRepositoryFilter">
    	<constructor-arg ref="sessionRepository" />
	</bean>

 

이 부분은 redis 설정을 다 했음에도 로그인 시(세션생성 코드 실행) redis-cli ->keys *로 세션을 확인했을때 아무것도 뜨지 않기때문에 추가해준 부분이다.

즉, redis설정이 되었음에도 계속해서 서버에 세션이 저장되고, redis에는 전혀 저장되지 않았기때문에 세션을 redis로 위임하는 기능이 필요했다. 

원래는 @Configuration과 @EnableRedisHttpSession 어노테이션만으로도 이게 자동으로 되어야한다고 하는데,

나는 내부에서 무언가 꼬였는지 잘 저장이 되지않았다. 그래서 수동으로 다시 한 번 설정해준 것이나 다름없다.

 

최종적으로 잘 설정되면 세션 생성될때마다 이렇게 keys에 값이 등록되는 것을 볼 수 있다. 

챗지피티는 그날 전체 1시간 30분이면 배포작업을 완료할 수 있다고 말했다..............그러나.........ㅋㅋㅋㅋ

 

며칠간 새 글을 쓰지 못했던 이유는 배포를 진행하며 남은 4월 기간동안 트러블 슈팅을 해보기 위함이었다. 

최초에 목표로 했던 것은

 

Docker를 사용한 배포(Spring 과 RDS사용)

RDS 사용이유?

더보기

왜 RDS?
일단 많은 회사들이 RDS를 써본 사람을 선호하는 것을 본 적이 있기도 하고, 

비용이 상대적으로 비싸긴 하지만, AWS가 성능을 최적화해주기 때문에 내가 특별히 신경쓰지 않아도 된다는 큰 장점.

보안도 AWS가 알아서 해주고, 장애복구도 AWS가 해주고, 버전관리도 내가 딱히 할 필요가 없다는 점이

빠른 배포 및 트러블슈팅을 원하는 나에게 가장 적합하다고 생각했기 때문이었다.

Docker 사용이유?

더보기

도커를 사용한 이유도 위와 비슷하다.

도커 사용경험이 있는 지원자를 선호하는 회사가 많고,

도커를 사용하면 서버 환경차이에 영향받지 않는다고 하며, 일관성/배포속도에서 압도적(이지 못했지만...)

그리고 추후 스케일 아웃 구조로 확장하기도 쉽다고 한다.

아, 그리고 Dockerfile로 환경설정을 해놓으면 이 구성이 자동화된다는 강점이 있었따.

Vercel을 활용한 프론트엔드 간단 배포

그리고 EC2에서 Nginx를 통해 Https 연동시키는 것이었다.

더보기

Nginx + HTTPS를 추가?

보안성을 좀 더 높여보고 싶었기 때문이다. 또한 내 실력향상 목표도 당연히 있음

현대의 프로토콜은 거의 https를 사용하므로 그에 맞추고 싶었다.

로컬 개발은 http로도 충분하지만, 실제 배포환경에 적합한 개발을 하고싶었기 때문

또한 나중에 해보니 http만 사용했을때는 'https가 아니라 위험하다'는 식의 알림이 계속 뜨기도 했다.

 

 

특히 Docker에서 Ngrinder를 사용해본 경험으로 Docker기반의 배포를 꼭 하고 싶었는데 문제가 발생했다.

비용문제로 EC2는 프리티어를 사용했는데, Docker를 돌리니까 지속적으로 SSH 연결이 끊기고 리소스 부족한 문제가 발생.

즉 EC2가 Docker를 견뎌내지 못하는 문제!

도커파일도 만들고 잘 하고있었지만 프리티어로는 어려움이 있었다.

그래서 아쉽지만 Docker는 사용하지 않고, EC2에 톰캣을 설치하는 일반적인 방식의 배포를 진행하기로 했다.

 

1차 배포후(톰캣 배포 + Vercel 배포) 실행했을때 https를 사용해야한다는 오류 발생

Nginx를 통해 HTTPS를 연동해주기로 했다. 

 

위를 성공해서 2차 배포하니, 지난번 팀플젝때 했던 것처럼 어찌저찌 로그인은 되지만 세션이 전혀 유지가 되지 않는 문제가 발생했다. 또한 CORS오류도 한 번 발생하여, @CrossOrigin 어노테이션 대신 전역적 CORS설정 하는 것으로 Config 파일 추가해주었다. 

 

세션 유지 되지 않는 문제는 login시에 서버에서 세션을 만들어내긴 하지만, 다음 api들에서는 세션이 하나도 유지되지 않기때문에 발생하는 문제였다. 지난번에도 해결하지 못했던 문제였기때문에 이번엔 꼭 해결하고 싶었다.

https://velog.io/@goat_hoon/React-Spring-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%84%B8%EC%85%98%EC%9D%B4-%EB%B6%88%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B4%EC%9C%A0

 

React + Spring - 로그인 세션이 불가능한 이유?

안되나봐..

velog.io

하지만 이런 글도 찾아버리고...ㅎㅎ

지난번 며칠간의 CORS 설정에 재설정에도 되지 않았던 경험을 떠올렸을때, 이번에는 조금 다른 방식이 필요하다는 생각이 들었다. 내가 지금 지향하는 것은 빠른 배포 및 트러블 슈팅이기에, 다른 방법을 찾아보다가 redis를 사용해서 세션을 서버가 아닌 다른 메모리에 저장하는 방법을 선택했다. 

 

그래서 며칠 동안 또 redis 설정을 했다. redis 설정은 다시 정리해서 올려보아야겠다.

 

redis  설정하면서 이번에도 느낀건 챗지피티만으로는 모든 것을 해결할 수가 없다는 것이다.

모르는 부분을 챗지피티만을 사용해서 해결하려고 하면 어느 순간 분명히 길을 잃는다. 내가 처음해보는 것들은 차라리 같은 시도를 해본 다른 블로그들을 참고해서 첫 토대를 잡고, 고쳐나가는 것만 챗지피티를 사용하는 편이 성공률이 높은 것 같다.

 

3차 배포후, redis에 세션이 잘 만들어져서 저장은 되지만, 다음 api에서 그 세션을 읽어오지 못해서 계속해서 세션을 생성하기만 하는 문제 발생.....이 과정에서 수많은 CookieFilter의 수정과, RedisConfig 수정과... swagger와 버전 안맞아서 버전 바꾸었다가 다시 되돌리기도 하고.. 몇번이나 EC2에 배포에 재배포를 했는지 모르겠다. 이 과정에서 자동배포라인을 구축하면 초기단계에 빠른 배포 및 추가개발에 좋겠다는 생각을 몇 번이나 한 것 같다. 

 

그리고 n차 배포후에, 쿠키가 전달되지 않는 것이 결국은 도메인 불일치로 인한 문제라는 것을 알게되었고 (request header에 set-cookie헤더가 절대절대 붙지않았다...그게 제일 필요한데ㅠㅠ) 프론트와 백을 같은 도메인으로 통합해서 cors 문제 자체를 없애버리는 방식을 택했다. 

비록 vercel을 활용한 프론트엔드의 간단배포는 물건너가서, 코드 수정하면 또 재배포해주어야하지만..

그래도 이제 안정적으로 배포에 성공했다.

그리고 도메인 이름도 vercel.app같은게 붙지 않으니 진짜 하나의 웹페이지같은 느낌을 준다.

가비아에서 도메인을 하나 샀는데, ~.site 사용료가 2500원이었다. ㅋㅋ 그래서 앞에 wherethereis라는 도메인명을 붙여서 사주었다 ㅋㅋㅋㅋ

더보기

도메인을 사야했던 이유는 

HTTPS 인증서(특히 무료 SSL 인증서인 Let's Encrypt)를 적용하기 위해서는 도메인 검증이 필요했기때문이다. 

도메인이 있어야만 인증서를 발급해주는 시스템이기 때문에!

그리고 좀 더 찾아보니, 브라우저는 IP주소 기반 HTTPS 연결을 기본적으로 신뢰하지 않는다고 한다.

그 외에도 ~~vercel.app과 같은 주소는 정돈되지 않은 느낌을 주기도하고, wherethereis.site는 외우기 쉬워서 사용자 접근이 용이하기도 하고... 비록 탄력적 IP를 할당하기는 했지만 도메인은 더더욱 고정적이기 때문에 지속가능성도 있다고 생각되었다.

그래서 더 쓰지못한 우여곡절도 많았지만, 드디어 최종 배포가 성공되었고 이제부터 트러블 슈팅이 본격 시작이다.

 

배포하면서 가장 뿌듯했던 부분은 드디어 세션이 안정적으로 유지된다는 사실

 

지금 모든 API 요청에서 같은 쿠키가 사용되는 것을 볼 수 있다! 매우 뿌듯

근데 열심히하면서 오류화면 캡쳐도 좀 해둘걸. 너무 많은 오류와 싸워와서 지금 뭐가얼마나 있었는지 기억이 나지를 않는다ㅠㅠ

 

여러가지 문제가 발생되고 있으므로 또 수정해나가야겠다:)

nGrinder 테스트를 진행하면서 테스트 인원이 단 1명일때에는 코드가 정상적으로 돌아가지만, 인원이 조금만 다수가 되어도 (심지어 5명일때에도) 테스트가 진행되다가 1분이 넘어가면 Connection Refused 에러가 뜨면서 테스트 자체가 어려워졌다.

이런저런 원인을 찾아 해결해보았음에도 해결되지 않았고, 마지막 원인으로 생각되는 것은 커넥션 누수

커넥션 누수를 해결하기 전에 왜 커넥션 누수가 발생하는지 정리해두려고 한다.

 

Connection Pool이란?

커넥션 풀은 데이터베이스 커넥션을 매번 새로 열고닫는 비용을 줄이기 위해서 미리 일정 수의 커넥션을 만들어두고 재사용하는 저장소이다. 즉 앱 시작시 미리 n개의 연결을 만들어두고, 웹 애플리케이션에서 요청이 오면 그 중 하나를 사용하고 끝나면 반납받는 시스템이라고 한다.

 

Connection Pool의 작동 구조는?

서버 실행시 커넥션 풀이 커넥션을 미리 만들어둔다.

사용자가 요청을 보내면 커넥션 풀에서 하나 대여한다(getConnection)

쿼리 실행 후 완료되면 반납(close())

반납된 커넥션은 다음 요청에서 재사용된다. 

 

커넥션 누수가 발생하는 이유?

커넥션을 빌린 후 반납하지 않으면, 계속 대여상태로 남고, 새 요청은 커넥션을 못받게 된다.

 

내가 커넥션 누수가 발생했다고 결론을 내린 이유는,

1. MySQL에서 sleep상태인 커넥션이 굉장히 많았고, 

2. 일정 횟수 이상의 테스트를 진행한 후에는 "Too many connections, Connection is not available"에러가 발생했기 때문이다.

 

실제로 MySQL에서 "show processList"를 쳐보면 다음과 같이 sleep중인 수많은 processList 들을 볼 수 있었다.

스크롤이 엄청 작다....정말 많은 processList들이 sleep상태에 있음을 볼 수 있다. 100개 이상이다....

실제로 오류 해결을 위해서 챗지피티의 도움을 받았을 때에도, 위 두 가지 오류가 발생한 경우 100%의 확률로 커넥션 누수가 발생한 것일 수 있다고 말한다. 

 

이는 애플리케이션 로직에서 커넥션 풀을 빌려서 쓴 후 반납하지 않았기 때문인데, 내부에서 커넥션 풀이 점점 고갈되어 남은 커넥션이 없기 때문이다. 

 

https://helloworld.kurly.com/blog/connection-leak/

 

99%가 모른다는 DB Connection 누수 문제

DB Connection과 Garbage Collector의 관계를 중심으로 mysql-connector-j 사용 시 발생할 수 있는 메모리 누수를 탐지하고 해결한 경험을 공유합니다.

helloworld.kurly.com

이 글을 참고하며 읽어보았다. 내 경우에는 메모리의 누수가 발생하는 것은 아니고, DB 커넥션의 누수만 발생하는 것이라고 한다.

 


그렇다면 어떻게 해결할 것인가?

챗지피티의 가이드를 따라가기로 했는데, 아래와 같다.

1. 누수 발생위치를 추적할 수 있는 로그 확인 코드 추가

2. 트랜잭션 누락 점검 (자동 풀 반납 보장)

3. 커넥션 점유상태 실시간 감시

 

1. leakDetectionThreshold로그로 누수위치 추적

<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
    <property name="jdbcUrl" value="jdbc:mysql:---" />
    <property name="username" value="---" />
    <property name="password" value="---" />
    <property name="leakDetectionThreshold" value="2000" /> <!-- 커넥션2초이상 점유시 경고메세지 출력 -->
    <property name="idleTimeout" value="30000" />
    <property name="maxLifetime" value="1800000" />
    <property name="maximumPoolSize" value="30" />
    <property name="minimumIdle" value="3" /> 
    <property name="connectionTimeout" value="30000" /> <!-- 커넥션 타임아웃 -->
</bean>

커넥션을 반납하지 않고 2초이상 점유시 경고메세지가 출력되고, 이 경고메세지를 통해 누수발생 위치를 파악할 수 있다고 한다.

 

2. @Transactional 어노테이션을 통한 트랜젝션/반납 자동처리

트랜젝셔널 어노테이션을 사용하면 Spring이 트렌젝션을 관리하게되고, 커넥션도 함께 열고 닫는 흐름을 직접 책임진다고 한다.

즉 아래와 같이 동작하며, 내가 커넥션을 직접 신경쓰지않아도 되게끔 스프링이 트렌젝션+커넥션을 같이 묶어서 관리해준다고 한다.

그 와중에 프록시 개념이 나오는데....

더보기

프록시란?
"실제 객체 앞에서 대신 요청을 받아 처리하는 대리인 객체"

 

스프링은 Transaction Proxy를 생성해서 트랜젝션을 자동으로 관리한다.

내가 서비스를 호출하면 프록시 객체가 먼저 호출을 가로채고, 아래와 같은 일을 한다고 한다.

 

 그리고 스프링이 프록시객체를 만들기 위해 사용하는 방법 중 하나가 JDK Dynamic Proxy! (JDK 동적 프록시)

인터페이스 기반인데, 이게 우리가 수업시간에 배웠던 내용이었다.

내가 이거 굳이 똑같은거 왜 만드냐고, 굳이 안만들어도 되냐고 쌤께 여쭤봤었는데, 쌤이 자세한 이유는 설명안해주시고

꼭 만드는게 좋다고 하셨었다. (아마 그때의 내가 들었으면 설명들으면서 멍때렸을듯ㅋㅋ)

 

@Service
public class TodoListServiceImpl implements TodoListService {
...
}

 서비스 interface를 만들어두고, 위와 같이 상속받는 것은 Spring AOP 프록시 작동을 위한 것이었다!!

 

*Spring AOP는 (Aspect-Oriented-Programming 관점지향 프로그래밍)으로 

공통적으로 반복되는 기능을 핵심 로직과 분리해서 따로 관리하는 프로그래밍 방식이라고 한다!

3. Show ProcessList로 실시간 커넥션 감시한다!

MySQL 창에서 show processList; 를 치면 sleep모드에 들어가있는 커넥션들을 모두 확인할 수 있다.

 

아! 그리고 중요한 것을 하나 깨달았는데, 예외처리 단순 try-catch만 하면 커넥션 누수가 발생할 수 있다는 것이다.

왜냐면 Transactional은 "체크되지 않은 예외(RuntimeException 이상)"이 발생했을때만 rollback을 수행하기 때문이라고 한다.

rollback을 수행해야만 정상적으로 풀을 반납한다고 한다! 

@Transactional
@Override  
public int todoWrite(TDdetailDTO dto) { //투두리스트 작성하는 기능, 성공시 결과값은 1
	
		int result =0;
		//System.out.println("service impl todoWrite : "+ dto.getTdId());
		try {
			result = tdMap.todoWrite(dto);
		} catch (Exception e) {
        	throw new RuntimeException("DB 작업실패", e);
		}
		if(result ==1) {
			tlMap.getLatest();
		}
	
	
	return result;
}

 

그래서 서비스 코드에 모두 @Transactional을 붙이고(특히 input과 같이 입력, 수정, 삭제하는 코드는 꼭 붙여야한다고 한다),

예외가 발생하는 지점에서는 단순 catch만하는 것이 아니라 예외를 던지는 방식으로 코드를 수정하였다.

 


코드 수정 이후, 커넥션 풀의 누수가 생기는지 확인해보았다.

일단 기존에 실행되고 있던 잠자고있는 프로세스 리스트를 모두 정리한다.

하나하나 kill하기엔 100개가 넘으므로, 서비스를 종료 후 재시작 하였다.

net stop mysql80
net start mysql80

//bash에서 실행하면 된다!

 

다시 show processList 명령어를 입력했을때 아래와 같이 단 두개 빼고 모두 사라진 것을 볼 수 있다.

 

그리고 다시 nGrinder를 실행시켰을때의 결과는....(1명은 성공하니까, 2명으로 진행했다)

 

그래도 1분30초 정도 되었을때부터 Connection Refused가 발생했으니 긍정적인 신호라고 보아야하나!

 

아니 내 서버는 2명도 버티지 못하는 코드인가....ㅠㅠ

자괴감이 든다 ㅠㅠ

1. 최초 코드를 사용한 nGrinder 테스트 결과

 

 

2. 코드 1차 수정 후(API 병합) 의 nGrinder 테스트 결과

 

3. Cache를 사용했을때의 nGrinder 결과는 아래와 같다.

 

 


최초코드의 성능테스트와 비교한 결과이다.

 

구분 개선 내용 TPS
(평균)
Peak
TPS
Mean
Time
(지연시간)
실행 수 비고
1차 (기존) API 3개 분리된 상태
(getTodo, getMemo, getProgress)
85.0 105 11.68 9,869 비효율적인 구조
2차
(API 통합)
/dashBoard/{tdDate} API 하나로 통합 99.6 123 9.96 11,557 중복 DB 조회 제거
3차
(캐싱 추가)
todoId를 날짜 기준 캐싱 104.2 128 9.52 12,092 DB 접근 완전 최소화

 

캐싱까지 추가하니 평균 응답시간이 9.52ms초로 약 19% 성능 개선된 것을 볼 수 있고,

TPS는 85에서 104.2로 23% 증가,

Peak TPS는 105에서 128로 증가하며 순간 처리능력이 향상된 것을 알 수 있었다. 

 

또한 그래프에서의 주목할 결과도 있었다.

TPS 그래프가 초방 상승 이후 꾸준히 100 이상을 유지하며 전체적으로 평탄한 모양을 보이는 것을 알 수 있다. 

또한 TPS와 PeakTPS의 차이가 크게 나지 않으며 서버가 지속적으로 고성능을 유지할 수 있음을 보여준다. 

 

이제 다음은 여러명에 대해서 실험할 차례이다.

이번에는 Cache에 sessionId와 날짜정보를 저장하고, 캐시가 있을 경우에는 db를 서치하지 않도록 코드를 수정해보았다.

 

//todoList의 todoId에 대한 캐시
    private final Map<String, Integer> todoIdCache = new ConcurrentHashMap<String, Integer>();
    private final ReentrantLock todoIdCacheLock = new ReentrantLock();
    
    public void putTodoIdCache(String sessionId, String tdDate, int todoId) {
    	String key = sessionId + "_" + tdDate; //캐시 키 생성
    	todoIdCacheLock.lock(); // 동기화를 위한 lock, 나만 접근할 수 있도록 문 잠그는 것
    	try {
			todoIdCache.put(key, todoId); //캐시에 저장
			System.out.println(key +"캐시 추가");
		} finally {
			todoIdCacheLock.unlock(); //작업 완료되었으면 해제
		}
    }
    //캐시에서 값을 가져오는 메서드 	
    public Integer getTodoIdCache(String sessionId, String tdDate) {
    	    String key = sessionId + "_" + tdDate;
    	    return todoIdCache.get(key);
    	}
    
    //캐시 전체를 초기화하는 메서드 
    public void clearTodoIdCache() {
    	   todoIdCacheLock.lock(); //캐시 전체 접근이므로 lock 사용
    	    try {
    	        todoIdCache.clear();
    	        System.out.println("todoId 캐시 초기화 완료!");
    	    } finally {
    	        todoIdCacheLock.unlock();
    	    }
    	}

 

여기서 

private를 쓴 이유는 외부 클래스에서 해당 Map에 접근할 수 없도록 은닉화 시킨것.

final을 쓴 이유는 이 변수에 한 번만 값을 대입할 수 있다는 뜻. 즉 다시 new HashMap() 이런 코드는 불가능함.

(todoIdCache.put은 가능함)

즉 이 코드는 생성 시 한 번만 초기화되고 절대 값이 바뀌지 않는다는 뜻이다. 

 

ConcurrentHashMap은 멀티스레드 환경에서 안전하게 사용할 수 있도록 만들어진 HashMap이라고 한다.

즉 여러 쓰레드가 동시에 읽고, 쓰는 상황에서도 오류없이 동작하게 만든 Map이다.

멀티쓰레드 상태에서 map.put이 동시에 발생하면, 데이터가 섞이거나 날아갈 가능성이 있다고 하는데,

ConcurrentHashMap은 그런 상황을 방지해준다!

 

이제 Controller에서 CacheConfig를 import 해주고, 사용하면 된다.

아래와 같이, 이번에는 autowired를 사용하지 않고 생성자 주입방식으로 변경해보았다.

 

package com.pj.planbee.controller;

@RestController
@RequestMapping("/todolist")
@CrossOrigin(origins="http://localhost:3000", allowCredentials ="true")
public class TodoListController {

	private final TodoListService ts;
	private final CacheConfig cacheConfig;


	public TodoListController(TodoListService ts, CacheConfig cacheConfig) {
		this.cacheConfig = cacheConfig;
		this.ts = ts;
	}

    @GetMapping(value = "/dashBoard/{tdDate}", produces = "application/json; charset=utf-8")
    public TodoDashboardDTO dashboardData(@PathVariable String tdDate, HttpSession se) {
    	String sessionId = (String) se.getAttribute("sessionId");
    	System.out.println("todolist sessionId" + sessionId);
    	TodoDashboardDTO dashboard = new TodoDashboardDTO();
    	int todoId;
    	//1. 캐시에서 먼저 검색한다
    	Integer savedId = cacheConfig.getTodoIdCache(sessionId, tdDate);
    	
    	//2. 캐시값이 null이 아니면, 그 캐시에 해당하는 todoId를 불러옴
    	if ( savedId != null ) { 
    		todoId = savedId;
    		System.out.println("캐시에 존재하는 값"+ todoId);
    	} else {
    		//3. 캐시값이 null이면 checkRow를 수행한다.
    		int result = ts.checkRow(tdDate, sessionId);
        	System.out.println("해당날짜에 해당하는 열번호"+ result);
        	if(result == 0) { //열이 없으면 한 열을 입력하고 그 결과를 반환한다.
        		ts.inputRow(tdDate, sessionId);
        		todoId = ts.tdIdSearch(tdDate, sessionId);
        	}else { //열이 있으면 그 결과값을 반환한다.
        		todoId = result;
        	}
        	//null인 경우 열 검색다 하고 캐시에 저장도 한다.
        	cacheConfig.putTodoIdCache(sessionId, tdDate, todoId);
        	System.out.println("캐시에 저장 완료" + todoId);
    	}
    	
    	
    	dashboard.setTodoId(todoId);
    	
    	//todoDetail들을 리스트 형식으로 가져온다.
    	List<TDdetailDTO> list = ts.getTodo(todoId); //한 줄로 쓰는게 더 개선된 부분이라고 함
    	dashboard.setTodoList(list);
    	System.out.println("그날짜의 todolist"+list);
    	
    	//todoMemo를 값으로 가져온다.
    	String memo = ts.getMemo(todoId);
    	dashboard.setMemo(memo);
    	
    	//진척도를 값으로 가져온다.
    	double progress = ts.todoProgress(todoId);
        ts.regiProgress(todoId, progress);
        dashboard.setProgress(progress);
        
    return dashboard;
    }
}

 

문제

 

생각한 방법 / 수도코드

s번째까지만 my_string을 출력한다.

overwrite_string을 출력한다.

s번째부터 나머지 my_string을 출력한다.

 

이 때 출력이 아니라 solution 함수를 작성하는 것이므로, 

return 값으로 답이 나와야한다.

 

String의 concat을 사용해서 기존 answer에 각각 글자를 갖다 붙인다.

 

완성된 코드

package bj_project;

import java.util.Scanner;

public class day0414 {
	public static void main(String[] args) {
		
	String answer = Solution.solution("Program29b8UYP", "merS123", 7 );
	System.out.println(answer);	
	}

}
class Solution {
	public static String solution (String my_string, String overwrite_string, int s) {
		String answer = "";
		
		for (int i = 0 ; i < s ; i ++) {
			answer = answer.concat(String.valueOf(my_string.charAt(i)));
		}
		answer = answer.concat(overwrite_string);
		for (int i = s + overwrite_string.length(); i < my_string.length(); i++) {
			answer = answer.concat(String.valueOf(my_string.charAt(i)));
		}
		return answer;
		
	}
	
}

결론 / 알게된 점

1. 메인클래스에서 Solution.solution()으로 쓰려면 class Solution 내의 solution 함수가 static이어야 한다.

static(정적) 메서드는  클래스이름으로 직접 호출이 가능하다. 

static을 사용하지 않으면 인스턴스 메서드가 되는데, 이 경우에는 new 로 객체를 생성한 다음에 호출해야한다.

그래서 static으로 선언해주었다.

 

2. String은 불변객체이기 때문에 concat()은 결합한 결과를 리턴할 뿐, answer 자체를 바꾸지 않는다고 한다.

그러므로 기존의 answer에 리턴값을 저장 해주어야한다.

이 부분 수정하였다. 


 

'알고리즘(백준)' 카테고리의 다른 글

1월 17일: 백준 20361번(JAVA)  (2) 2025.01.18
1월 16일 : 백준 25191번(JAVA)  (1) 2025.01.16
1월 13일: 백준 11098번(JAVA)  (4) 2025.01.13
1월 12일: 백준 27434번(JAVA-> PYTHON)  (1) 2025.01.12
1월 12일: 백준 11365번  (1) 2025.01.12

+ Recent posts