<개요>

- 신규 프로젝트 진행시 Spring Boot 3.2 를 사용하기 위해서 공통 프로젝트 생성

- 기존 프로젝트와도 호환성 및 추후 유지보수를 위해서 버전을 올리기로 결정

(엊그제 시작했던 것 같은데 벌써 Support 기간이 끝난 Legacy 가 되어감.. 버전업 속도가 참 빠르다)

- 연계되어 있는 Artifact들이 많고 Compatibility를 확인해야 함 (JDK버전까지 고려해야 함)

Spring Cloud AWS Spring Cloud Spring Boot Spring Framework AWS Java SDK
2.3.x (maintenance) 2020.0.x (3.0) 2.4.x, 2.5.x 5.3.x 1.x
2.4.x (maintenance) 2021.0.x (3.1) 2.6.x, 2.7.x 5.3.x 1.x
3.0.x 2022.0.x (4.0) 3.0.x, 3.1.x 6.0.x 2.x
3.1.x 2023.0.x (4.0) 3.2.x 6.1.x 2.x

 

<내용>

  • Legacy Version
    • spring boot : 2.4.3
    • spring-cloud : 2020.0.2
    • spring-cloud-aws : 2.3.0
    • aws sdk v1, v2
  • Target Version
    • spring boot : 3.2.4
    • spring-cloud : 2023.0.0
    • spring-cloud-aws : 3.1.1
    • aws sdk v2
  1. Code변경사항
    • Spring 6.x
      1. 패키지명
        1. javax → jakarta
      2. Security Config
        1. cors, httpBasic, csrf, antMatchers
          1. depreacted 메소드 수정 및 주입방식 변경
      3. jakarta.annotation.PostConstruct
        1. tomcat-annotations추가가 필요한데 버전은 추가 확인할 필요가 있다.
      4. HttpStatus::is4xxClientError
        1. HttpStatusCode::is4xxClientError 변경
      5. java.lang.NoSuchMethodError: 'void org.springframework.util.Assert.notNull(java.lang.Object)’
        1. spring boot parent만 사용하고 참조라이브러리들 특정버전을 명시하는 것은 삼가한다.
        2. jasypt의 경우 spring 내부 encrypt를 사용하는 것으로 점차 변경할 예정.
      6. PagingAndSortingRepository 가 더이상 CrudRepository를 상속하지 않기 때문에 추가필요
    • Hibernate
      1. Unable to load class [org.hibernate.dialect.MySQL57Dialect]
        1. org.hibernate.dialect.MySQLDialect 로 변경
      2. Alias [creationDateTime] is already used in same select clause [position=4]
        1. 중복되는 컬럼이 있는지 체크하는 부분이 추가됨
      3. spring.datasource.initialization-mode=always is deprecated.
      4. spring: jpa: defer-datasource-initialization: true sql: init: mode: always data-locations: classpath:data-local.sql
      5. Custom으로 작성한 쿼리에서 간혹 자동 생성Count쿼리에 문제가 발생하는 경우가 있다.  기본 Select 문법에 대한 수정이 필요함 (e.g Alias 등)
    • Swagger 변경 (기존 Swagger 2안에 javax 패키지 참조로 오류가 발생하여 변경이 필요함)
      1. maven 변경
      <dependency>    
      	<groupId>org.springdoc</groupId>    
      	<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>    
      	<version>2.0.2</version></dependency>
      <dependency>
          <groupId>io.swagger</groupId>
          <artifactId>swagger-annotations</artifactId>
          <version>1.5.20</version>
      </dependency>
      
    • MySQL
      • <dependency>
      • <groupId>com.mysql</groupId>
      • <artifactId>mysql-connector-j</artifactId>
      • </dependency>
    • AWS SDK V2
      1. 상세 사용법은 AWS Documentation을 참고하여 수정함
    • Spring Security
      • WebFlux SecurityWebFilterChain
        • 기존에는 @Configuration이 없어도 동작했었으나 반드시 필요함
      • 테스트를 위한 Bean 생성 및 방법
@RunWith(SpringRunner.class)
@WebMvcTest(Controller.class )
@Import(SecurityConfig.class)
@ContextConfiguration(classes = SpringSecurityWebAuthTestConfig.class)
public class ControllerTest { 
	@Test @WithUserDetails("admin")
@TestConfiguration
@EnableWebSecurity
public class SpringSecurityWebAuthTestConfig { 
	@Bean 
    @Primary 
    public UserDetailsService userDetailsService() { 
    List<SimpleGrantedAuthority> userAuth = new ArrayList<>(); 
    userAuth.add(new SimpleGrantedAuthority("ROLE_USER")); 
    List<SimpleGrantedAuthority> adminAuth = new ArrayList<>(); 
    adminAuth.add(new SimpleGrantedAuthority("ROLE_ADMIN")); 
    
    LoginUserDetails loginUserDetails = new LoginUserDetails(-1, "", "user","", userAuth); 
    LoginUserDetails loginAdminDetails = new LoginUserDetails(-1, "", "admin", "", adminAuth); 
    
    return new CustomInMemoryUserDetailsManager(Arrays.asList(loginUserDetails, loginAdminDetails)); 
}

 

- 참조 사이트

https://github.com/awspring/spring-cloud-aws?tab=readme-ov-file#compatibility-with-spring-project-versions

 

GitHub - awspring/spring-cloud-aws: The New Home for Spring Cloud AWS

The New Home for Spring Cloud AWS. Contribute to awspring/spring-cloud-aws development by creating an account on GitHub.

github.com

https://spring.io/projects/spring-boot#support

 

Spring Boot

 

spring.io

https://docs.aws.amazon.com/ko_kr/sdk-for-java/latest/developer-guide/migration-whats-different.html

 

AWS SDK for Java 1.x와 2.x의 차이점은 무엇입니까? - AWS SDK for Java 2.x

SDK 1.x의 여러 패키지 이름에는 다음이 포함됩니다. v2 이 v2 경우에 를 사용한다는 것은 일반적으로 패키지의 코드가 서비스 버전 2에서 작동하도록 타겟팅되었음을 의미합니다. 전체 패키지 이

docs.aws.amazon.com

https://github.com/aws/aws-sdk-java-v2/blob/master/docs/LaunchChangelog.md#411-s3-operation-migration

 

aws-sdk-java-v2/docs/LaunchChangelog.md at master · aws/aws-sdk-java-v2

The official AWS SDK for Java - Version 2. Contribute to aws/aws-sdk-java-v2 development by creating an account on GitHub.

github.com

https://github.com/quarkusio/quarkus/wiki/Migration-Guide-3.0:-Hibernate-ORM-5-to-6-migration

 

Migration Guide 3.0: Hibernate ORM 5 to 6 migration

Quarkus: Supersonic Subatomic Java. . Contribute to quarkusio/quarkus development by creating an account on GitHub.

github.com

https://docs.spring.io/spring-boot/docs/3.2.4/reference/html/dependency-versions.html

 

Dependency Versions

 

docs.spring.io

 

<개요>

  • 테스트의 크기는 되도록이면 작게
  • 각 테스트 단위는 독립적으로 주입할 수 있도록
  • 필요한 것만 주입받고 테스트 해야하며
  • 최대한 빠르게 테스트 실행이 가능해야 함

<내용>

  • Layer
    • Controller
      • Web을 통한 호출시 PathVariable, RequestBody, Header, 인증등 을 담당하는 역할
    • Service
      • Biz Logic 구현
      • Biz Transaction 이 필요할 경우
    • Repository
      • Storage 와 Application 의 다리역할
      • 객체변환, 타입검증
    • POJO
      • 독립된 구현
      • 주로 Input / Output 을 통합 로직 수행
  • 공통사항
    • Given / When / Then 으로 작성하면 편함 (준비-실행-검증)
  • 각 Layer 별 테스트 코드는 다른 레이어가 정상동작한다는 가정으로 테스트하는 것이 원칙
    • Controller Layer
      • WebMvc에 관련된 Context만 로딩 (WebMvcTest)
      • 사용할 Bean들만 TestConfiguration 으로 정의하여 Context의 가동범위를 최소한 으로 한다.  
@RunWith(SpringRunner.class)
@WebMvcTest(BizController.class
)
@Import(SecurityConfig.class)
@ContextConfiguration(classes = SpringSecurityWebAuthTestConfig.class)
public class BizControllerTest {

    @Autowired
    private MockMvc mvc;
    @MockBean
    private BizService bizService;
   
    @Test
    @WithUserDetails("admin")
    public void getAllData() throws Exception{
        List<DataDto> datas = new ArrayList<>();

        DataDto dataDto = new DataDto();
        dataDto.setDataId(1);
        String dataName = "ttt";
        dataDto.setDataName(dataName);
        dataDto.setUserId(1);
        datas.add(dataDto);

        Page<DataDto> pages=new PageImpl<>(datas, Pageable.unpaged(), 1);

        //given
        given(bizService.findAllDatas(PageRequest.of(0,1))).willReturn(pages);
        //when , then
        this.mvc.perform(get("/datas/all?page=0&size=1"))
                .andExpect(jsonPath("content").isNotEmpty())
                .andExpect(jsonPath("content[0].dataId").value(1))
                .andExpect(status().isOk());
    }
    • 테스트 메소드 작성
      • MockBean : Mockup 대상
      • given : 테스트 범위내에서 정상동작할 경우의 응답, 혹은 주어진 조건
      • when : mvc.perform : 수행
      • then : andExpect : 기대값
  • Service Layer
    • JUnit으로만 테스트 (Spring Mvc 필요없음)
    • 테스트 대상인 Service 만 Inject, 나머지는 Mock
@RunWith(MockitoJUnitRunner.class)
public class BizServiceTest {
    @Mock
    private BizRepository dataRepository;
    @Mock
    private ModelMapper modelMapper;
    @InjectMocks
    private BizService bizService;

    @Test
    public void createService() throws Exception {
        DataDto dataDto = new DataDto();
        dataDto.setDataId(1);
        dataDto.setUserId(1);
        dataDto.setDataName("text");
        
        DataEntity dataEntity = new DataEntity();
        dataEntity.setDataId(1);
        dataEntity.setUserId(1);
        dataEntity.setDataName("text");
				
        //given
        given(modelMapper.map(dataDto, DataEntity.class)).willReturn(dataEntity);
        given(modelMapper.map(dataEntity, DataDto.class)).willReturn(dataDto);
        //when
        DataDto result = dataService.createData(dataDto);
        //then
        Assert.assertEquals(dataDto, result);

    }
      • 테스트 메소드 작성
        • given : 테스트 범위내에서 정상동작할 경우의 응답, 혹은 주어진 조건
        • when : 테스트 대상
        • then : assertEquals ( expected, actual)
  • Repository Layer
    • DataJpaTest 관련된 Context만 로딩
    • 사용할 Bean들만 정의하여 가동범위 최소한으로
    @RunWith(SpringRunner.class)
    @DataJpaTest
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    @Import({EnableEncryptablePropertiesConfiguration.class, JasyptConfig.class, SpringSecurityWebAuthTestConfig.class, TestJpaAuditingConfig.class})
    public class NewsRepositoryTest {
    
        @Autowired
        private NewsRepository newsRepository;
    
        @Test
        public void findAllByEnabled(){
            //given
            Pageable pageable = PageRequest.of(0,10);
    	//when
            Page<NewsEntity> newsEntityPage = newsRepository.findAllByEnabled(true, pageable);
    	//then
            Assert.assertEquals(1, newsEntityPage.getTotalElements());
        }
    • 테스트 메소드 작성
      • given : 테스트 범위내에서 정상동작할 경우의 응답, 혹은 주어진 조건
      • when : 테스트 대상
      • then : assertEquals ( expected, actual )
    • Local Test의 경우 H2 나 기타 메모리DB로 기동될때마다 테스트 데이터를 넣어놓으면 독립적인 테스트가 가능하기 때문에 편리하다.
  • 독립 Module
    • Context 기동없이 가능
    public class EmailValidationTest {
    
        @Test
        public void validation(){
            Pattern codePattern = PatternValidator.ValidationType.EMAIL.getMyPattern();
    
            Matcher matcher = codePattern.matcher("terst@gmail.com");
            Assert.assertTrue(matcher.matches());
    
            matcher = codePattern.matcher("test-1@naver.com");
            Assert.assertTrue(matcher.matches());
    • Bean주입없이 그냥 Java new로 POJO 테스트
public class JasyptTest {

	@Test
	public void encryptDecrypt() throws Exception {
		PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
		SimpleStringPBEConfig config = new SimpleStringPBEConfig();
		config.setPassword("");
		config.setAlgorithm("PBEWithMD5AndDES");
		config.setKeyObtentionIterations("1000");
		config.setPoolSize("1");
		config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
		config.setStringOutputType("base64");
		encryptor.setConfig(config);

		String raw = "abcdefg";
		String encrypted = encryptor.encrypt(raw);

		String decrypted = encryptor.decrypt(encrypted);
		Assert.assertEquals(raw , decrypted);
	}

 

Git Remote Repository에 Push하기전에 Local Test Case를 모두 통과하는지 반드시 확인해야 한다.

<개요>
- 다음과 같이 Service #A 에서 Service #B로 데이터 조회 API를 요청하고 값을 받아오는 로직이 있다.
- Service #B에서는 AWS Athena를 저장소로 사용하고 있으며 Athena JDBC42 드라이버를 사용 중 이다.

API 호출 후 응답


<현상>
- Service #B에서 JdbcTemplate을 통하여 쿼리가 수행된 시간은 11:13:13 이고,
2021-11-04 11:13:13.482 DEBUG 9668 --- [http-nio-8200-exec-9] o.s.jdbc.core.JdbcTemplate : Executing SQL query
2021-11-04 11:13:13.482 DEBUG 9668 --- [http-nio-8200-exec-9] o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
- 실제 쿼리 수행결과를 받아온 시간은 11:15:57 로 약 2분44초 가 소요되었다.
2021-11-04 11:15:57.998 INFO 9668 --- [http-nio-8200-exec-9] ...

- Athena 의 경우 동시에 다수의 쿼리가 수행되면 Queue에 의하여 순차적으로 수행될 수 있기 때문에 쿼리 히스토리를 조회하였다.

11:13:13.542초 시작, 수행시간 0.555초
대기열시간 1분21초

- 대기열 시간 1분21초 + 수행시간 0.555초를 제외하고 꽤 오랜시간이 소요되었다.

<소스분석>
- AthenaJDBC42의경우 일반적인 JDBC드라이버처럼 커넥션을 맺고 Resultset을 처리하는 형태가 아니라 AWS Athena로 Http를 통해서 수행요청을 하고, 리턴값으로 ID를 받아온 뒤 일정시간 Thread Sleep하면서 조회 polling을 요청하고 Status가 Completed가 되었을때 후속처리를 하는 형태로 구성되어 있다.

- 또한 위에도 언급한것처럼 동시에 다수의 요청이 집중될경우 자체적으로 큐에 보관하여 처리하게 된다.

- 부수적으로 Athena JDBC드라이버의 SStatement내 execute, getResultSet등의 메소드를 살펴보면 대부분 synchronized로 선언이 되어있기 때문에 이에 따른 delay도 있지 않을까 예상한다.

 

<Thread Dump>

10개의 Thread가 같은 위치에서 대기중이다.

"http-nio-8200-exec-9" #44 daemon prio=5 os_prio=31 tid=0x00007ffcc655f800 nid=0x8c03 waiting on condition [0x000070000c638000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.simba.athena.athena.api.AJClient.executeQuery(Unknown Source)
at com.simba.athena.athena.dataengine.AJQueryExecutor.execute(Unknown Source)
at com.simba.athena.jdbc.common.SStatement.executeNoParams(Unknown Source)
at com.simba.athena.jdbc.common.SStatement.executeNoParams(Unknown Source)
at com.simba.athena.jdbc.common.SStatement.executeQuery(Unknown Source)
- locked <0x000000078740ccf8> (a com.simba.athena.athena.jdbc42.AJ42Statement)
at com.zaxxer.hikari.pool.ProxyStatement.executeQuery(ProxyStatement.java:111)
at com.zaxxer.hikari.pool.HikariProxyStatement.executeQuery(HikariProxyStatement.java)
at org.springframework.jdbc.core.JdbcTemplate$1QueryStatementCallback.doInStatement(JdbcTemplate.java:439)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:376)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:452)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:462)
at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:473)
at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:480)


<정리>
- 다수의 사용자에게서 발생하는 ad-hoc형태 처리는 적합하지 않다.(hive와 동일함)

- Global cache(Redis)를 적절히 활용하여 Service #B Layer에서 처리를 하도록 하면 효율성을 증가시킬수 있다.(일반적인 캐시전략)

- Red Shift등의 빠른대안도 있으나 가성비가 매우 떨어진다.


+ Recent posts