windows pc에 node를 설치하고 Windows PowerShell에서 작업을 하려고 하면 

이 시스템에서 스크립트를 실행할 수 없으므로 C:\Program Files\nodejs\npm.ps1 파일을 로드할 수 없습니다.

이 오류가 나는 경우가 있다.

PowerShell의 정책으로 인한 문제이기 때문에 정책설정을 변경 해주면 된다.

PS C:\WINDOWS\system32> Set-ExecutionPolicy RemoteSigned

 

'Development Story > Software' 카테고리의 다른 글

VMware Workstation 17 Player 설치  (0) 2024.07.04

Spring Boot에서 Session Timeout 설정은 application.properties에 설정을 하면 된다.

spring.session.timeout=1800
server.servlet.session.timeout = 60m

설정은 spring Boot에서 session.timeout만 설정을 해도 되지만 설정이 적용이 안되면 server.servlet.session.timeout을 설정을 한다.

was단에서는 web.xml에 설정을 하면 application.properties에 별도로 설정을 안해도 된다.

<session-config>
       <session-timeout>30</session-timeout>
</session-config>

 

또는 Was 서버 설정에서 해도 된다.

 

Redis를 적용 했을 시 만약 모든 설정이 적용이 안될 수도 있다.

그럴 경우에는 Redis Config에 강제 설정을 해주면 된다.

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 6000) // 30분
@Profile("prod")
public class RedisConfig {

Spring Boot에 Security를 설정으로 중복 로그인 방지 기능 적용 시 로컬 Session을 사용하지 않고 Redis를 사용 시

세션 정책을 ConcurrentSessionControlAuthenticationStrategy 를 사용하면 중복 로그인 체크가 불안정 해서 세션 정책을 

CompositeSessionAuthenticationStrategy를 적용하면 불안정 했던 중복로그인 체크가 안정화가 됐다.

 

1. SecurityConfig CompositeSessionAuthenticationStrategy,  ConcurrentSessionControlAuthenticationStrategy Bean 등록

// 세션 정책 설정
@Bean
public CompositeSessionAuthenticationStrategy sessionAuthenticationStrategy(SessionRegistry sessionRegistry) {
          List<SessionAuthenticationStrategy> strategies = new ArrayList<>();
           strategies.add(concurrentSessionControlStrategy(sessionRegistry));
           strategies.add(new SessionFixationProtectionStrategy());
           strategies.add(new RegisterSessionAuthenticationStrategy(sessionRegistry));
           return new CompositeSessionAuthenticationStrategy(strategies);
}

@Bean
public ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy(SessionRegistry sessionRegistry) {
           ConcurrentSessionControlAuthenticationStrategy strategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
           strategy.setMaximumSessions(1);
           strategy.setExceptionIfMaximumExceeded(false);
           return strategy;
}

 

2. SessionExpiredStrategy Class Component 구현

SessionExpiredStrategy Class는 먼저 로그인 한 유저가 세션이 Expired 될때 후처리를 할 수 있는 Class이다.

@Component
public class CustomSessionExpiredStrategy implements SessionInformationExpiredStrategy {

            @Override
            public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
                       HttpServletResponse response = event.getResponse();
                       HttpServletRequest request = event.getRequest();
                       response.sendRedirect("이동URL?expired-session=true");
            }

}

 

3. SecurityConfig SecurityFilterChain 수정

//중복 로그인 Custom Session Expired
private final CustomSessionExpiredStrategy customSessionExpiredStrategy;


@Bean
public SecurityFilterChain filterChain( HttpSecurity http , AuthenticationManager authenticationManager,
CompositeSessionAuthenticationStrategy sessionStrategy) throws Exception {
            .
            .
            .
            .

             // 세션 관리 설정
             http.sessionManagement( sessionManagement -> sessionManagement
                          .sessionCreationPolicy( SessionCreationPolicy.IF_REQUIRED )
                          .enableSessionUrlRewriting( false )
                          .sessionFixation().changeSessionId()
                          .maximumSessions( 1 )
                          .expiredSessionStrategy(customSessionExpiredStrategy)
                          //.expiredUrl( "이동URL?invalid-session=true" ) //CustomSessionExpiredStrategy 에서 처리하기 대문에 주석
                          .maxSessionsPreventsLogin( false )
                          .sessionRegistry( sessionRegistry)
             );

            // Filter 적용 시 SessionRegistry를 필터에 적용을 해줘야 정책이 반영된다.
             http.addFilterAfter( customAuthenticationFilter( authenticationManager, sessionStrategy ) , UsernamePasswordAuthenticationFilter.class );


              return http.build();
}


@Bean
public CustomAuthenticationFilter customAuthenticationFilter ( AuthenticationManager authenticationManager,
CompositeSessionAuthenticationStrategy sessionStrategy) throws Exception {

               CustomAuthenticationFilter  filter = new CustomAuthenticationFilter  ();
               filter.setAuthenticationManager( authenticationManager );
               filter.setFilterProcessesUrl( "/member/login-cert-processing" );
               filter.setUsernameParameter( "mbrId" );
               filter.setPasswordParameter( "mbrPswd" );
               filter.setAuthenticationSuccessHandler( customAuthenticationSuccessHandler() );
               filter.setAuthenticationFailureHandler( customAuthenticationFailureHandler() );
               filter.setSessionAuthenticationStrategy(sessionStrategy); // 세션 관리 전략 설정

               return filter;

}

 

 

ConcurrentSessionControlAuthenticationStrategy나 CompositeSessionAuthenticationStrategy 둘다 기본적으로 Session 개수를 제한 하는 정책을 수립 할 수 있는데 ConcurrentSessionControlAuthenticationStrategy 정책이 왜 Redis에서 불안정 한지 잘은 모르겠다.

이전에는 격지 않았는데 이번에 네이버클라우드 Redis 서비스, Wildfly를 사용을 하면서 불안정 한건지는 모르겠지만

중복로그인을 처리 할 때 중복체크가 불안정하면 CompositeSessionAuthenticationStrategy 정책으로 변경하면 된다.

 

개인적으로 여러 세션 전략을 적용 할 수 있는 CompositeSessionAuthenticationStrategy를 사용하는 것이 좋을거 같다.

Spring을 사용하면 Security에서 손쉽게 중복 로그인 방지 설정을 할 수 있다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager
,ConcurrentSessionControlAuthenticationStrategy sessionStrategy) throws Exception {

.....생략

// 세션 관리 설정
       http.sessionManagement(sessionManagement -> sessionManagement
             .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
              .enableSessionUrlRewriting(false)
              .sessionFixation().changeSessionId()
              .maximumSessions(1)
              .expiredUrl("/login?invalid-session=true")
              .maxSessionsPreventsLogin(false)
              .sessionRegistry(sessionRegistry())
       );

       return http.build();
}

 

1. sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)

  • 역할:
    • Spring Security가 세션을 생성하는 시점을 정의합니다.
  • SessionCreationPolicy 옵션:
    • ALWAYS: 요청마다 새 세션을 강제로 생성.
    • NEVER: Spring Security가 세션을 생성하지 않지만 이미 존재하는 세션은 사용.
    • IF_REQUIRED: 인증이 필요하면 세션을 생성 (기본값).
    • STATELESS: 세션을 전혀 생성하지 않음. JWT 기반 인증 같은 무상태(stateless) 애플리케이션에서 사용.
  • 적용 효과:
    • 이 설정으로 세션은 "필요할 때만" 생성되며, 세션이 없는 상태에서 인증 요청이 들어오면 새로 생성됩니다.

2. enableSessionUrlRewriting(false)

  • 역할:
    • 세션 ID를 URL에 추가하는 것을 허용할지 여부를 설정.
  • 배경:
    • URL에 세션 ID가 포함되면 브라우저 간 세션 공유, 세션 하이재킹(Session Hijacking) 같은 보안 문제가 발생할 수 있음.
  • 적용 효과:
    • false: URL에 세션 ID를 포함하지 않음. 쿠키 기반 세션 유지만 허용.
    • 보안 강화를 위해 보통 false로 설정.

3. sessionFixation().changeSessionId()

  • 역할:
    • 인증 과정에서 세션 고정(Session Fixation) 공격을 방지하기 위해 기존 세션 ID를 변경.
  • 배경:
    • 공격자가 사용자의 세션 ID를 미리 예측하거나 가로채서 악의적인 작업을 수행할 수 있음.
  • 적용 효과:
    • changeSessionId(): 기존 세션의 속성을 유지하면서 새로운 세션 ID를 생성.
    • newSession(): 기존 세션을 무효화하고 새로운 세션 생성.
    • none(): 세션 ID를 변경하지 않음 (비권장).
  • 권장:
    • changeSessionId()는 보안성과 성능의 균형이 맞아 자주 사용됨.

4. maximumSessions(1)

  • 역할:
    • 한 사용자당 동시에 유지 가능한 세션 수를 제한.
  • 적용 효과:
    • 여기서는 1로 설정되어, 한 사용자가 한 번에 하나의 세션만 가질 수 있음.
    • 다른 브라우저나 기기에서 동일한 계정으로 로그인하면 이전 세션이 만료됨.

5. expiredUrl("/login?invalid-session=true")

  • 역할:
    • 사용자의 세션이 만료되었을 때 리디렉션될 URL을 설정.
  • 적용 효과:
    • 예를 들어, 동일한 계정으로 다른 브라우저에서 로그인하면 기존 세션은 만료되고 /login?invalid-session=true로 리디렉션됩니다.
    • 사용자 경험을 개선하기 위해 "세션 만료됨" 메시지를 이 URL에서 처리할 수 있음.

6. maxSessionsPreventsLogin(false)

  • 역할:
    • 세션 제한을 초과했을 때 새로운 로그인을 허용할지 결정.
  • 적용 효과:
    • false: 새로운 로그인 요청을 허용. 기존 세션은 만료됨.
    • true: 새로운 로그인 요청을 차단. "이미 최대 세션 수에 도달했습니다" 같은 에러를 반환.
  • 권장 사용:
    • 일반적으로 false로 설정하여 사용자가 다른 기기에서 로그인할 수 있도록 함.

7. sessionRegistry(sessionRegistry())

  • 역할:
    • SessionRegistry는 활성 세션을 추적하고 관리하는 데 사용됩니다.
    • 이 설정은 Spring Security의 동시 세션 제어(Concurrent Session Control)에 필요.
  • 적용 효과:
    • 사용자당 활성 세션 목록을 유지하며, 특정 사용자의 모든 세션을 만료시키거나, 만료된 세션 정보를 추적할 수 있음.
    • Spring Security의 SessionRegistryImpl을 기본 구현으로 사용.

적용 예

  1. 사용자가 이미 로그인된 상태에서 다른 브라우저에서 로그인:
    • 기존 세션은 만료되고 /login?invalid-session=true로 리디렉션.
    • 새로운 세션이 생성되어 인증이 유지됨.
  2. 사용자가 최대 세션 제한을 초과:
    • maxSessionsPreventsLogin(false)로 인해 새 로그인이 허용되고, 이전 세션은 종료됨.

 

필터를 사용 하게 되면 필터에 session registry를 별도로 설정을 해줘야 한다.

 

1. SecurityConfig에 ConcurrentSessionControlAuthenticationStrategy Bean 추가

@Bean
public ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy(SessionRegistry sessionRegistry) {
        ConcurrentSessionControlAuthenticationStrategy strategy =
new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
        
        
strategy.setMaximumSessions(1);

         strategy.setExceptionIfMaximumExceeded(false);
         return strategy;
}

 

2. SecurityConfig에 filterChain에 ConcurrentSessionControlAuthenticationStrategy sessionStrategy 추가하고 Filter에 전달

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager
,ConcurrentSessionControlAuthenticationStrategy sessionStrategy) throws Exception {

.....생략

// 세션 관리 설정
       http.sessionManagement(sessionManagement -> sessionManagement
             .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
              .enableSessionUrlRewriting(false)
              .sessionFixation().changeSessionId()
              .maximumSessions(1)
              .expiredUrl("/login?invalid-session=true")
              .maxSessionsPreventsLogin(false)
              .sessionRegistry(sessionRegistry())
       );

     
        http.addFilterBefore(CustomAuthenticationFilter(authenticationManager, sessionStrategy), UsernamePasswordAuthenticationFilter.class);
      

       return http.build();
}


@Bean
public CustomAuthenticationFilter CustomAuthenticationFilter(AuthenticationManager authenticationManager
,ConcurrentSessionControlAuthenticationStrategy sessionStrategy) {
        KsignGpkiCertAuthenticationFilter filter = new KsignGpkiCertAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager); // AuthenticationManager 설정
        filter.setFilterProcessesUrl("/member/ksign-gpki-login-processing"); // 필터가 처리할 URL 설정
        filter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler()); // 인증 성공 핸들러 설정
        filter.setAuthenticationFailureHandler(customAuthenticationFailureHandler()); // 인증 실패 핸들러 설정
        filter.setSessionAuthenticationStrategy(sessionStrategy); // 세션 관리 전략 설정
        return filter;
}

 

필터 등록 시 세션 관리 전략을 설정하지 않으면 중복 로그인 방지가 작동하지 않는다.

1. pom.xml dependency 설정

<!-- Redis 설정 -->
<dependency>
         <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
          <groupId>org.springframework.session</groupId>
          <artifactId>spring-session-data-redis</artifactId>
</dependency>

 

2. application.properties 설정

################################################
# Redis 서버 설정
################################################
spring.redis.host=127.0.0.1 (redis server host)
spring.redis.username=redis user 계정 (계정 설정이 별도로 없으면 설정 자체를 삭제)
spring.redis.password=redis 패스워드
spring.redis.port=6379 (redis server 설정 port)
# Spring Session이 세션 데이터를 저장하기 위해 사용할 스토리지 유형을 지정
spring.session.store-type=redis
# Redis에 저장되는 데이터의 직렬화 형식을 JSON으로 설정
spring.redis.serializer=json
# Redis에 세션 데이터를 저장(플러시)하는 시점을 설정
# on-save : 세션 객체가 수정될 때 Redis로 데이터를 저장하며, 성능 상의 이점을 제공
spring.session.redis.flush-mode=on-save

 

레디스 서버를 로컬에 적용을 하지 않을 경우에는 레디스 설정을 삭제하고 redis 사용안함으로 설정

spring.session.store-type=none
spring.cache.type=none

 

3. RedisConfig Class 설정

@Configuration
@EnableRedisHttpSession
@Profile("prod")
public class RedisConfig {

            @Bean
           public ConfigureRedisAction configureRedisAction() {
                        return ConfigureRedisAction.NO_OP;
            }

}

위 설정은 운영서버에서만 적용이 되게 설정 RedisConfig 이다.

ConfigureRedisAction 클래스와 NO_OP 설정은 Spring Session에서 Redis를 설정할 때 Redis Keyspace Notifications(키스페이스 알림) 관련 작업을 처리하는 방법을 정의하는 데 사용하는데 클라우드에서 제공하는 Redis에서는 지원을 하지 않아서 오류가 발생 할 수 있다.
사용하는 Redis에서 지원을 하지 않으면 해당 설정으로 기능을 사용 안함으로 적용해야 한다.

 

4. Session에 적재되는 객체 직렬화

Redis Session을 사용 할 경우에는 객체를 직렬화 해야 한다. Java 기본 자료구조는 직렬화가 되어 있어서 문제가 되지 않지만 Java Bean을 만들어서 Session에 담으려고 하면 직렬화를 해야 세션에 담고 빼는데 문제가 없다.

Java Bean을 만들때 특별한 사유가 없으면 implements Serializable를 해주면 된다.

Spring Boot에서 Jta Tansaction Manager를 설정해서 개발 하던 중에 어플리케이션을 실행 하면 tmlog가 생성이 된다.

UserTransactionServiceImp를 Bean으로 등록을 해서 로그가 생성이 되는데 로그를 생성하지 않거나 atomikos에 미리 정의 되어 있는 설정을 변경 하고 싶을 시 Properties를 새로 정의 해서 UserTransactionServiceImp를 init 하면 된다.

Tansaction  properties파일에 셋팅을 하면 된다고도 하고 application.properties에 설정을 하면 된다고 하는데 UserTransactionServiceImp Bean으로 등록을 별도로 정의 하면 init 순서에 따라서 properties에 설정 한 값이 셋팅된 값이 적용이 안될 경우에는 구현한 Tansaction Manager에 직접 설정을 하면 된다.

 

@Configuration
@Slf4j
public class JtaTransactionManagerConfig {

       @Autowired
        private Environment env;

        @Bean
        @DependsOn("userTransactionService") // userTransactionServiceImp가 먼저 초기화되도록 설정
        public PlatformTransactionManager transactionManager() throws Exception {

                log.info("transactionManager() 실행");

                // UserTransaction 설정

                 UserTransactionImp userTransaction = new UserTransactionImp();
                 userTransaction.setTransactionTimeout(300); // 300초로 트랜잭션 타임아웃 설정
                 // UserTransactionManager 설정
                 UserTransactionManager userTransactionManager = new UserTransactionManager();
                 userTransactionManager.setForceShutdown(false); // 애플리케이션 종료 시 강제 종료하지 않음
                 // JTA 트랜잭션 매니저 설정
                 JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(userTransaction, userTransactionManager);
                 // 커밋 실패 시 롤백 설정
                 jtaTransactionManager.setRollbackOnCommitFailure(true);
                 return jtaTransactionManager;
         }

         @Bean(initMethod = "init", destroyMethod = "shutdownForce")
         public UserTransactionServiceImp userTransactionService() {

                  // UserTransactionServiceImp 설정
                  UserTransactionServiceImp userTransactionServiceImp = new UserTransactionServiceImp();

                  // 트렌젝션 로그파일 생성 안되게 설정
                  Properties properties = new Properties();
                  properties.setProperty("com.atomikos.icatch.enable_logging", "false");
                  properties.setProperty("com.atomikos.icatch.log_base_dir", env.getProperty("com.atomikos.icatch.log_base_dir"));

                  userTransactionServiceImp.init(properties);

                  return userTransactionServiceImp;
         }

}

 

Bean으로 등록하지 않은 경우에는 properties에 com.atomikos.icatch 관련 설정을 직접 하면 된다.

 

참고로 로그를 생성하고 싶지 않을 경우 com.atomikos.icatch.log_base_dir 로그 디렉토리 설정 값을

 

Window는 NUL로 설정 하고 Linux는 /dev/null로 설정을 해야 해서 spring maven profile 설정에 locla과 서버 배포 설정에

 

값을 설정해서 config 설정에 사용 했다.

#리눅스
com.atomikos.icatch.log_base_dir
=/dev/null

#Window
com.atomikos.icatch.log_base_dir
=NUL

 

네이버 클라우드에서 서비스 되는 CloudOutBoundMailer API는 http로 직접 호출과 네이버 클라우드에서 지원하는 라이브러리를 활용 할 수 있다.
네이버 클라우드 공식 사이트에서는 라이브러리 가이드는 별도로 제공하지 않고 jar를 다운로드 받으면 jar의 소스코드도 같이 압축이 되어 있는데 해당 소스코드를 분석해서 메일발송 유틸을 만들어야 한다.
 

1. 라이브러리 다운로드

https://guide-gov.ncloud-docs.com/docs/email-email-1-3

Cloud Outbound Mailer API reference - Cloud Outbound Mailer

guide-gov.ncloud-docs.com

 

2. Cloud Out Bound Mailer 가이드

https://guide-gov.ncloud-docs.com/docs/email-email-1-1

Outbound Mailer 소개

guide-gov.ncloud-docs.com

https://api-gov.ncloud-docs.com/docs/ai-application-service-cloudoutboundmailer

Cloud Outbound Mailer 개요

api-gov.ncloud-docs.com

 

3. Spring Boot Ncp Cloud Out Bound Mailer 구현

3.1. Maven Dependency

nes-client-1.6.0G.jar를 WEB-INF/lib 경로에 복사 후 dependency 추가

<!-- 네이버 클라우드 mail Api 라이브러리 -->
<dependency>
         <groupId>com.nbp.ncp</groupId>
          <artifactId>nes-client</artifactId>
          <version>1.6.0G</version>
          <scope>system</scope>
          <systemPath>${project.basedir}/src/main/webapp/WEB-INF/lib/nes-client-1.6.0G.jar</systemPath>
</dependency>

 

3.2. Build 시 외부 jar이 포함 될 수 있게 Maven 설정

링크 참조 : https://kkubi-story.tistory.com/13

Spring boot Maven 외부 라이브러리 추가

Spring boot Maven 설정파일 pom.xml에 외부 라이브러리 추가 설정 org.springframework.boot spring-boot-maven-plugin true spring-boot-maven-plugin에 includeSystemScope를 true로 설정 com.groupid artId 버전 system ${project.basedir}/lib/

kkubi-story.tistory.com

 

4. NcpCloudOutboundMailerService 구현

import java.nio.file.Paths;
import java.util.Map;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import com.nbp.ncp.nes.ApiClient;
import com.nbp.ncp.nes.ApiResponse;
import com.nbp.ncp.nes.api.V1Api;
import com.nbp.ncp.nes.auth.PropertiesFileCredentialsProvider;
import com.nbp.ncp.nes.exception.ApiException;
import com.nbp.ncp.nes.marshaller.FormMarshaller;
import com.nbp.ncp.nes.marshaller.JsonMarshaller;
import com.nbp.ncp.nes.marshaller.XmlMarshaller;
import com.nbp.ncp.nes.model.EmailSendRequest;
import com.nbp.ncp.nes.model.EmailSendResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;


@Slf4j

@Service
@RequiredArgsConstructor
public class NcpCloudOutboundMailerService extends EgovAbstractServiceImpl{

        private final TemplateEngine templateEngine;

         //NCP Outbound Mailer Api Iam Credentials properties 경로
         @Value("${ncp.cloud.outbound.mailer.iam.credentials.path}")
         private String ncpIamCredentialsPath;

         @Value("${mail.default.sender.address}")
         private String senderAddress;

         @Value("${mail.default.sender.name}")
         private String senderName;

       /**
         * Html로 작성된 mail 템플릿을 String 으로 반환
         *
         * @author limmyungho
         * @since 24.09.20
         *
         * @param templatePath html로 작성된 mail 템플릿 경로
         * @param templateModel html로 작성된 mail 템플릿에 전달할 map 데이터
         * @return String
         * @throws Exception
         */
         public String getHtmlMailTemplateString(String templatePath, Map<String, Object> templateModel) throws Exception{

                  Context context = new Context();
                  context.setVariables(templateModel);

                  String bodyText = templateEngine.process(templatePath, context);

                  return bodyText;

         }

       /**
         * Html로 작성된 mail 템플릿을 String 으로 반환
         *
         * @author limmyungho
         * @since 24.09.20
         *
         * @param requestBody mail 데이터
         * @return ApiResponse<EmailSendResponse>
         * @throws Exception
         */
         public ApiResponse<EmailSendResponse> mailSend(EmailSendRequest requestBody) throws Exception{

                  String resourcePath = Paths.get(this.ncpIamCredentialsPath).toAbsolutePath().toString();

                  ApiClient apiClient = new ApiClient.ApiClientBuilder()
                           .addMarshaller(JsonMarshaller.getInstance())
                           .addMarshaller(XmlMarshaller.getInstance())
                           .addMarshaller(FormMarshaller.getInstance())
                           .setCredentials(new PropertiesFileCredentialsProvider(resourcePath).getCredentials())
                           .setLogging(true)
                           .build();

                  V1Api v1Api= new V1Api(apiClient);

                  //requestBody.setTemplateSid({templateID}); 템플릿을 만들었다면 템플릿 번호
                  requestBody.setSenderAddress(this.senderAddress);
                  requestBody.setSenderName(this.senderName);
                  requestBody.setConfirmAndSend(false);

                  String X_NCP_LANG = "ko-KR"; // String | 언어 (ko-KR, en-US, zh-CN), default:en-US
                  System.out.println(requestBody);

                  try {

                           ApiResponse<EmailSendResponse> result = v1Api.mailsPost(requestBody, X_NCP_LANG);

                           return result;

                  } catch (ApiException e) {
                           String msgCode = "ncp.outbound.mailer.400.status";

                           EmailSendResponse emailSendResponse = new EmailSendResponse();
                           emailSendResponse.setCount(0);
                           emailSendResponse.setRequestId("");

                           ApiResponse<EmailSendResponse> result = new ApiResponse<EmailSendResponse>(e.getHttpStatusCode(), e.getHttpHeaders(), emailSendResponse);

                           f(result.getHttpStatusCode() == 500) {
                                    msgCode = "ncp.outbound.mailer.500.status";
                           }
                           
                           //전자정부 프레임워크 예외 발생 시 후처리 가능하게 Pass 해줌
                           leaveaTrace(msgCode);

                           return result;
                  }

         }

}

 
ncpIamCredentialsPath는 네이버클라우드에서 발급된 apiKey, accessKey, secretKey 정보가 있는 설정 파일이다.
api 발급 타입이 iam이면 apiKey는 없어도 무관하다.

properties 파일 내용

type=iam

apiKey=
accessKey=ncp_iam_BGAMKR5UvVVojp4pYJeT
secretKey=ncp_iam_BGKMKRSqjWDVwqV3zoiyjxi0OvNGTARAEl

 

5. 발송 테스트 시 라이브러리 오류 대응

발송 테스트 시 특정 라이브러리에서 오류가 발생 할 경우 충돌, 버전, No Class 시 꼭 필요한 라이브러리 Maven에 설정되어 있는지 확인

<dependency>
        <groupId>com.squareup.okhttp3</groupId>
         <artifactId>okhttp</artifactId>
</dependency>

<dependency>
         <groupId>com.squareup.okhttp3</groupId>
         <artifactId>logging-interceptor</artifactId>
</dependency>


<dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
         <groupId>com.fasterxml.jackson.dataformat</groupId>
         <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

* 위 라이브러리는 네이버클라우드 메일 발송에 필요한 라이브러리이다.

spring boot batch에서 JtaTransactionManager를 사용하면 아래와 같은 에러가 발생 할 수 있습니다.

org.springframework.transaction.InvalidIsolationLevelException: JtaTransactionManager does not support custom isolation levels by default - switch 'allowCustomIsolationLevels' to 'true'

 

해당 오류가 발생 하는 이유는 JtaTransactionManager는 이기종 Database를 하나의 클로벌 트랜잭션으로 관리를 하기 때문에 특정 데이터베이스 격리 수준을 지원하지 않기 때문입니다.

 

JtaTransactionManager에 대한 내용은 아래의 글을 참고하면 됩니다.

https://kkubi-story.tistory.com/25

 

ChainedTransactionManager과 JtaTransactionManager

이전에는 ChainedTransactionManager를 사용 했는데 2.5버전 이후부터 Deprecated되어서 소스에 줄이 생기는 것이 보기 싫어서 JtaTransactionManager로 변경을 했습니다.그리고 TransactionManager를 변경을 하면서 Cha

kkubi-story.tistory.com

 

위 오류에서는 allowCustomIsolationLevels를 true로 설정을 하라는 메세지인데 JtaTransactionManager에 격리 수준을 사용 하겠다는 설정을 true로 추가 해주면 됩니다.

 

@Bean
public PlatformTransactionManager transactionManager(UserTransaction userTransaction, TransactionManager transactionManager) {
    JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(userTransaction, transactionManager);
    jtaTransactionManager.setAllowCustomIsolationLevels(true);  // 커스텀 격리 수준 허용
    return jtaTransactionManager;
}

 

jtaTransactionManager.setAllowCustomIsolationLevels(true); 을 추가 하면 커스텀 격리 수준을 허용하겠다는 것이고 Spring boot batch에서 위 오류는 발생하지 않는다.

 

트랜잭션 격리 수준(Isolation Level)은 데이터베이스에서 트랜잭션 간의 간섭을 제어하기 위해 사용됩니다. 트랜잭션 격리 수준은 여러 트랜잭션이 동시에 실행될 때 발생할 수 있는 문제들을 제어하는 방식에 따라 네 가지로 분류됩니다. 각 격리 수준은 데이터 일관성과 동시성 사이에서 균형을 맞추고 있으며, 낮은 격리 수준은 더 높은 동시성을 제공하지만, 잠재적으로 더 많은 데이터 불일치를 허용할 수 있습니다.

 

1. READ UNCOMMITTED (읽기 미완료)

  • 설명: 가장 낮은 수준의 격리 수준으로, 하나의 트랜잭션에서 변경된 데이터가 커밋되지 않았더라도 다른 트랜잭션에서 읽을 수 있습니다. 이는 "더티 리드(Dirty Read)"라고 하는 현상을 허용합니다.
  • 특징:
    • 동시성: 가장 높음
    • 일관성: 가장 낮음
    • 문제점: 더티 리드(커밋되지 않은 데이터를 다른 트랜잭션에서 읽을 수 있음)
  • 사용 시나리오: 일관성보다는 성능이 더 중요한 경우 사용합니다. 예를 들어, 보고서 생성이나 로그 분석과 같은 경우.

2. READ COMMITTED (읽기 완료)

  • 설명: 대부분의 데이터베이스에서 기본적으로 사용되는 격리 수준입니다. 하나의 트랜잭션에서 변경된 데이터는 커밋이 완료된 후에만 다른 트랜잭션에서 읽을 수 있습니다. "더티 리드"는 발생하지 않지만 "논리적 불일치" 문제는 발생할 수 있습니다.
  • 특징:
    • 동시성: 높음
    • 일관성: 중간
    • 문제점: 논리적 불일치(Non-repeatable Read, 하나의 트랜잭션에서 두 번 이상 데이터를 읽을 때 값이 다를 수 있음), 팬텀 리드(Phantom Read)
  • 사용 시나리오: 대부분의 애플리케이션에서 기본적으로 사용됩니다. 트랜잭션이 짧고, 성능과 일관성 간의 균형이 중요할 때 적합합니다.

3. REPEATABLE READ (반복 가능한 읽기)

  • 설명: 트랜잭션이 시작된 후에 다른 트랜잭션이 해당 트랜잭션에서 읽은 데이터를 변경하거나 삭제할 수 없습니다. 이는 "논리적 불일치" 문제를 방지하지만 "팬텀 리드"는 여전히 발생할 수 있습니다.
  • 특징:
    • 동시성: 중간
    • 일관성: 높음
    • 문제점: 팬텀 리드(Phantom Read, 트랜잭션 중간에 데이터가 삽입되면 해당 데이터가 보일 수 있음)
  • 사용 시나리오: 트랜잭션이 동일한 데이터를 여러 번 읽고, 해당 데이터의 일관성이 중요한 경우 사용됩니다. 예를 들어, 은행 계좌의 잔액을 여러 번 확인하는 경우.

4. SERIALIZABLE (직렬화 가능)

  • 설명: 가장 높은 수준의 격리 수준으로, 트랜잭션이 직렬화된 것처럼 실행됩니다. 즉, 트랜잭션이 순차적으로 실행되는 것처럼 동작합니다. 이 격리 수준에서는 모든 트랜잭션이 독립적으로 실행되므로 팬텀 리드와 논리적 불일치 문제를 모두 방지할 수 있습니다.
  • 특징:
    • 동시성: 낮음
    • 일관성: 매우 높음
    • 문제점: 성능 저하, 높은 잠금 충돌 가능성
  • 사용 시나리오: 일관성이 매우 중요한 경우, 성능을 희생하더라도 데이터 무결성을 완전히 보장해야 하는 경우 사용됩니다. 예를 들어, 금융 거래 시스템에서 사용될 수 있습니다.

요약

격리 수준문제 해결동시성데이터 일관성

READ UNCOMMITTED 없음 높음 낮음
READ COMMITTED 더티 리드 방지 중간 중간
REPEATABLE READ 논리적 불일치 방지 낮음 높음
SERIALIZABLE 모든 문제 방지 매우 낮음 매우 높음

각 격리 수준은 데이터베이스의 트랜잭션 관리에서 동시성과 일관성 간의 균형을 어떻게 맞출 것인가를 결정합니다. 애플리케이션의 요구 사항에 따라 적절한 격리 수준을 선택하는 것이 중요합니다.

 

+ Recent posts