728x90

재직중인 회사에서 개발 서버 배포중  아래와 같은 에러가 발생하였다. 

OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000fbe00000, 27262976, 0) failed; error='Not enough space' (errno=12)

 

위에 러는 말그대로 메모리가 충분하지 않아 발생한다. 

 

해결책으로는 여러가지 방법이 있는걸로 화인되었다.

 

  • 시스템의 메모리 로드 줄이기 
  • 물리적 메모리 또는 SWAP 공간 늘리기
  • SWAP 백킹 저장소가 가득차 있는지 확인
  • 64비트 OS 64비트 Java 사용 
  • JAVA 힙 크기 줄이기 (-Xmx/ -Xms)
  • java 스레드 수 줄이기
  • java 스레드 스택 크기 줄이기 (-Xss)

다양한 방법이 있는데 나의 겨우   swap 공간을 확인후 할당하는 방향으로 해결하기로 했다. 

swap 이란?

스왑은 Linux 기반 운영 체제에서 가상 메모리로 작동하는 저장 장치(예: HHD, SSD, 가상 저장 장치)의 전용 공간이다. 시스템의 사용 가능한 메모리가 부족할 때 물리적 RAM(Random Access Memory)을 보충하는 데 사용된다. 스왑 공간을 통해 운영 체제는 덜 자주 사용되는 데이터를 RAM에서 스왑 영역으로 이동하여 더 중요하거나 자주 액세스하는 데이터를 위해 RAM의 공간을 확보할 수 있다. 

스왑 공간은 스왑 파티션 또는 스왑 파일의 형태일 수 있다. 스왑 파티션은 저장 장치의 전용 파티션인 반면 스왑 파일은 기존 파일 시스템 내의 파일이다. 둘 다 동일한 목적을 수행한다.

 

 

메모리 및 swap 메모리 확인

 

아래와 같이  dd 명령어로 파일 생성

dd if=/dev/zero of=/var/spool/swap/swapfile bs=1MiB count=2048

 * 해당 디렉토리가 없는 경우  직접 디렉토리 생성

 

번에 1MiB(1MB)씩 2048번 반복하여 null문자로 채워진 파일을 만든다. (1MB * 2048 = 2GB)

fallocate 명령어를 이용하여 대용량 파일 생성도 가능 하나 리눅스 버전에 따라 swapfile 인식 안될수 있어

dd 명령어와 /dev/zero 활용하여 swap 파일을 만드는 것을 권장한다.

 

퍼미션  변경

sudo chmod 600 /var/spool/swap/swapfile

swap 포맷 변환 및 swap 할당

sudo mkswap /var/spool/swap/swapfile
sudo swapon /var/spool/swap/swapfile

 

 

 

 

swap 파일 시스템 등록(필요한 경우)

vim /etc/fstab
UUID=09934798-af72-4741-b12f-aa84a5850aa1  /    ext4    defaults    1 1
/var/spool/swap/swapfile                none    swap    defaults    0 0

*swapon 명령어를 이용하면 시스템에 swap파일을 등록할 수 있지만, OS를 재부팅할 경우 다시 리셋이 됨.

swap파일 등록을 영구히 하기 위해서는, 리눅스 파일시스템 테이블에 등록하면 가능.
/etc/fstab 에는 swap파일 이외에 마운트폴더 영구 적용.

위의 코드의 맨아래줄과 같이 swap파일을 등록하면 끝.

 

 

 

 

 

 

최종 적용 결과

 

 

 

 

728x90

'코드 > dev' 카테고리의 다른 글

OSI 7계층  (0) 2025.04.02
네트워크 기초  (4) 2024.07.23
배포시 테스트코드 profile 적용 안되는 오류  (2) 2024.07.10
sync vs async, blocking vs non-blocking 차이  (0) 2024.04.13
@async와 @transactional  (0) 2024.04.01
728x90

이전에 다니던 회사에서는 배포시 test 코드 실행에 대해 큰 걱정을 하지 않았는데 야믈 파일에 환경변수가 설정이 되어  배포 되는 서버에서 환경번수를 넣어주기떄문에 

개발 서버 빌드시 개발 config 환경변수가 들어가고 운영서버 빌드 시에는 운영 config 환경변수가 들어오는

 

현재는 빌드시 해당 지정 profile을 읽고 있다. 

 

이떄 문제가 빌드시에는 default profile 을 읽고 있기떄문에 test 코드 실행시 문제가 되기 때문에  test 코드 실행 시 동적으로 읽을 필요가 있다. 

아래는 실제 Test 실패로 나온 경우다.

 

> Task :test

AsyncExceptionHandlingTest > 슬랙 알람 확인 FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:132
        Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
            Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
                Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
                    Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
                        Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:658
                            Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:185
                                Caused by: org.redisson.client.RedisConnectionException at ConnectionPool.java:153
                                    Caused by: java.util.concurrent.CompletionException at CompletableFuture.java:368
                                        Caused by: io.netty.channel.ConnectTimeoutException at AbstractNioChannel.java:261

HowserPartnerApiApplicationTests > contextLoads() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:132
        Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
            Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
                Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
                    Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
                        Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:658
                            Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:185
                                Caused by: org.redisson.client.RedisConnectionException at ConnectionPool.java:153
                                    Caused by: java.util.concurrent.CompletionException at CompletableFuture.java:368
                                        Caused by: io.netty.channel.ConnectTimeoutException at AbstractNioChannel.java:261

HowserTemplateTest > 쿠팡 OpenApiKey 유효성 테스트 FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:132
        Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
            Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
                Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
                    Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
                        Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:658
                            Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:185
                                Caused by: org.redisson.client.RedisConnectionException at ConnectionPool.java:153
                                    Caused by: java.util.concurrent.CompletionException at CompletableFuture.java:368
                                        Caused by: io.netty.channel.ConnectTimeoutException at AbstractNioChannel.java:261

 

 

문제를 해결할 방법을 찾아보니  여러 방법이 있지만 

일단 현 시점에 가장 맞는 2가지 방법을 찾았다. 

 

1. ActiveProfilesResolver를 이용하여 test 코드 빌드 시 동적 profile을 적용하도록 변경

먼저 profile 을 동적으로 적용해주기 위해  ActiveProfilesResolver를 구현

 

import org.springframework.test.context.ActiveProfilesResolver;
 
public class SpringActiveProfilesResolver implements ActiveProfilesResolver {
    private static final String SPRING_PROFILES_ACTIVE = "spring.profiles.active";
 
    @Override
    public String[] resolve(Class<?> testClass) {
        String property = System.getProperty(SPRING_PROFILES_ACTIVE);
        return new String[] {property};
    }
}
//위 파일을 넣기 위해서는 dependency spring test 추가 필요

 

위와 같이 생성 후  아래와 같이 테스트 클래스를 만들어 준다. 

import com.example.demo.resolver.SpringActiveProfilesResolver;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
 
@SpringBootTest
@ActiveProfiles(resolver = SpringActiveProfilesResolver.class)
class ActiveProfilesTest {
	@Value("${spring.config.activate.on-profile}")
    private String profile;
 
    @Test
    @DisplayName("ActiveProfilesResolver를 이용하여 값을 local로 세팅하고 테스트한다.")
    void test() {
        assertEquals(profile, "prod");
    }
}

 

위와같이 생성이 완료되면 

 

아래와 같이 VM 옵션을 적용하여 확인해준다. 

 

나의 경우는 build 시에 필요하여  빌드에 해당 옵션 넣어서 테스트를 진행하였다. 


 

2번째 방법

gradle test task  옵션  추가 아래 실제 추가 이미지

 

위와 같이 속성 지정 후 1번 방법의  vm 추가하면 잘 적용되는것을 확인 할 수 있다. 

 

항상 설정을 잘 확인하고 정확하게 알고 사용하자..

 

 

728x90
728x90

재직중인 회사에서  네이버사 연계중 초당 10회의 제한있어 이를 처리 하는 로직을 수정을 진행 하였고 동기, 비동기, 블로킹, 논블로킹을 다시 리마인드 할겸 정리를 한다. 

 

동기/비동기 와 블로킹 논블로킹 이 두 개념은 표현 형태는 비슷해 봉리지라도, 서로 다른 차원에서 작업의 수행방식을 설명하는 개념이다.

 

작업을 순차적으로 수행할지 아닌지에 대한 관점이고, 블로킹/ 논블로킹은 단어 그대로 현재 작업이 block(차단, 대기) 되느냐 아니냐에 따라 다른 작업을 수행할 수 있는지에 대한 관점이다.

 

 

 

1.동기(sync) 와 비동기 (Async)

 

 

Synchronous의 Syn 그리스어로 '함께'이란 뜻이고 chrono '시간'이라는 뜻이다.

즉, Synchronous는 작업 시간을 함께 맞춰서 실행한다 라는 뜻으로 해석된다. 작업을 맞춰 실행한다는 말은 요청한 작업에 대해 완료 여부를 따져 순차대로 처리하는 것을 말한다. 

Asynchronous는 앞에 A라는 접두사가 붙어 부정하는 형태이다. 그래서 동기와 반대로 요청한 작업에 대해 완료 여부를 따지지 않기 때문에 자신의 다음 작업을 그대로 수행하게 된다.

 

 

비동 기 처리 이점

위와 같이 순차처리 와 다르게 비동기 처리시 Task1이 끝나는 시점에 끝나기 떄문에 2배 이상 빠른 처리 가능

 


 

2. 블로킹(Blocking)과 논블로킹(Non-Blocking)

 

현재 작업을 차단 혹은 대기 하냐 안하냐의 차이를 나타내는 프로세스 실행 방식.

동기/비동기가 전체 작업의 순차 흐름 유무. 블로킹/논블로킹 은 전체 작업의 흐름 자체를 막냐 안막냐 의 차이이다.

 

비동기와 논블로킹의 차이

console.log("시작");

setTimeout(() => {
  console.log("0.5초 비동기/ 논블로킹");
}, 500);

console.log("끝");

 

script   setTimeout 을 예를 들면 

출력 순서와 정의된 코드 라인 순서가 맞지 않은 것이다. 

이는 setTimeout 함수에 대해 타이머 작업 완료 여부를 신경 쓰지 않고 바로 그 다음 콘솔 작업을 수행하였기 때문이다. 그리고 setTimeout 함수의 타이머 작업 완료 알람을 콜백 함수를 통해 값을 받아 출력하였다. 따라서 setTimeout 은 비동기(Asynchronouse)이다.

 

 

setTimeout 함수는 자신의 타이머 작업을 수행하기 위해 메인 함수를 블락하지 않고 백그라운드에서 별도로 처리되었다. 메인 함수를 블락하지 않으니 setTimeout 함수를 호출하고 바로 그 다음 콘솔 함수를 호출한 것이다. 따라서 setTimeout 은 논블로킹(Non-blocking)이다.

 


 

3. 동기/비동기 + 블로킹 논블로킹

 

  • Sync Blocking(동기 + 블로킹)
  • Async Blocking(비동기 + 블로킹)
  • Sync Non-Blocking(동기 + 논블로킹)
  • Async Non-Blocking(비동기 + 논블로킹)

 

 

 

Sync Blocking 

다른 작업이 진행되는 동안 자신의 작업을 처리하지 않고(Blocking) 다른 작업의 완료 여부를 바로 받아 순차적으로 처리하는 방식

 

Async Non-Blocking

 

다른 작업이 진행되는 동안에도 자신의 작업을 처리하고( Non Blocking), 다른 작업의 결과를 바로 처리하지 않아 작업 순서가 지켜지지 않는  Async 방식

 

 

Sync Non-Blocking 

다른 작업이 진행되는 동안에도 자신의 작업을 처리하고( Non Blocking) 

다른 작업의 결과를 바로 처리하여 작업을 수낯대로수행하는  sync 방식

 

async Blocking 

 

다른 작업이 진행되는 동안 자신의 작업을 멈추고 기다리는( Blocking),  다른 작업의 결과를 바로 처리하지 않아 순서대로 작업을 수행하지 않는  Async 방식 (실제 로  sync blocking  과 큰 차이가 없고 작업할때 볼일이 없을 듯 하다).

 

 

 

728x90
728x90

재직 중인 회사에서 작업 후 잘못 작업 한 부분이 있어  이를 확인하고 정리 할려고 한다. 

spring @Transaction, @Async 2가지  어노테이션을 사용법이나 주의 사항을 정리하고자 한다.

 

 

@Transactional, @Async  흔하게 많이 쓰는 어노테이션이다. 

 

 

 

Trasction 이란 데이터베이스의 상태를 변화시키기 위해서 수행하는 작업의 단위를 뜻한다. 

 

Spring에서 트랜잭션 관련된 3가지 핵심 

1. 동기화

2. 추상화

3. AOP이용한 트랜잭션 분리 

 

 

동기화 

 

트랜잭션을 시작하기 위한  connection 객체를 특별한 저장소에 보관해두고 필요할때 꺼내쓸 수 있도록 하는 기술 

 

//트랜잭션 매니저  시작 

TransactionSynchronizeManager.initSynchronization();
Connection c = DataSourceUtils.getConnection(dataSource);



//종료
DataSourceUtils.releaseConnection(c, dataSource);
TransactionSynchronizeManager.unbindResource(dataSource);
TransactionSynchronizeManager.clearSynchronization();

 

트랜잭션 동기화 저장소는 작업 쓰레드마다 Connection 객체를 독립적으로 관리, 멀티쓰레드 환경에서도 충돌이 발생할 여지가 없다. 

*Hibernate 에서는 Connection이 아닌 Session 을씀 이로 인한 JDBC  종속적인 트랜잭션 동기화 코드들을 문제를 유발

이를 해결하기 위해 아래 기술할 추상화 기술 제공.

 


 

추상화

 

Spring은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술 제공.  이를 통해  JDBC, JPA, Hobernate 등 종속적인 코들르 이용하지 않고 일관되게 트랜잭션을 처리 가능.

 

 

트랜잭션 경계설정을 위한 추상 인터페이스는 Platform TransactionManager 이다.

 


 

AOP 

 

흔히 우리가 쓰는 @Transactional 어노테이션이다. 이 어노테이션을 사용 함으로써 마치 트랜젹션 코드가 존재하지 않는 것 처럼 보이지만 실제로 트랜잭션이 적용이 되는것이다.

 

 


 

트랜잭션 세부 설정

 

Spring의 DefaultTrasactionDefinition이 구현하고 있는 TrasctionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의 하고 있다. 아래 4가지 속성은 세부적으로 이용할 수 있게 도와주며, @Transactional 어노테이션에도 공통적으로 적용할 수 있다. 

 

1. 트랜잭션 전파

2. 격리수준

3. 제한시간

4. 일기전용

 


트랜잭션 전파

 

트랜잭션 전파란 트랜잭션의 경계에서 이미 진행중인 트랜잭션이 있거나 없을 때 어떻게 동작할 것인가를 결정하는 방식을 의미.

예를 들어 A 작업에 대한 트랜잭션이 진행중이고 B 작업에 대한 트랜잭션을 어떻게 처리할까에 대한 부분이다.

 

A의 트랜잭션 참여 (PROPAGATION_REQUIRED)

B의 코드는 새로운 트랜잭션을 만들지 않고 A에서 진행중인 트랜잭션에 참여할 수 있다. 이 경우 B의 작업이 마무리 되고 나서, 남은 A의 작업(2)를 처리할 때 예외가 발생하면 A와 B의 작업이 모두 취소된다. 왜냐하면 A와 B의 트랜잭션이 하나로 묶여있기 떄문이다.

 

독립적인 트랜잭션 생성(PROGATION_REQUIRES_NEW)

반대로 B의 트랜잭션은 A의 트랜잭션과 무관하게 만들 수 있다. 이 경우 B의 트랜잭션 경계를 빠져나오는 순간 B의 트랜잭션은 독자적으로 커밋 또는 롤백되고, 이것은 A에 어떠한 영향도 주지 않는다. 즉 이후 A가 (2)번 작ㅇ버을 하면서 예외가 발생해 롤백되어도 B의 작업에는 영향을 주지 못한다.

 

 

 

트랜잭션 없이 동작(PROGATION_NOT_SUPPORTED)

B의 작업에 대해 트랜잭션을 걸지 않을 수 있다. B의 작업이 단순 데이터 조회라면 굳이 트랜잭션이 필요 없을 것이다.

 


 

격리 수준

 

모든 DB 트랜잭션은 격리 수준을 가지고 있어야한다. 서버에는 여러 개의 트랜잭션이 동시에 진행 될 수 있는데, 모든 트랜잭션을 독립적으로 만들고 순차 진행 한다면 안전하지만 성능이 떨어짐. 따라서 적절하게 격리수준을 조정해서 가능한 많은 트랜잭션을 동시에 진행시키면서 문제가 발생하지 않도록 제어해야 한다. 이는 JDBC  드라이버나 DataSource  등에서 재설정하고 트랜잭션 단위 로 격리 수준을 조정할 수 있다.

DefualtTrasactionDefinition에 설정된 격리 수준은 ISOLATION_DEFAULT로 DataSource에 정의된 격리 수준을 따른다는 것이다.

기본적으로는  기본 격리 수준을 따르는 것이 좋지만. 특별한 작업을 수행하는 메소드라면 독자적으로 지정해줄 필요가 있다.

 

 


 

제한시간

 

트랜잭션을 수행하는 제한시간을 설정할 수 있다.  제한시간의 설정은 트랜잭션을 직접 시작하는 PROGATION_REQUIRED나 PROGATION_REQUIRES_NEW의 경우에 사용해야만 의미가 있다.

 


 

읽기전용

읽기전용으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아주며 데이터 엑세스 기술에 따라 성능 향상이 된다.

(재직중인 곳이 아니더라도 보통 SELECT 문 을 처리할떄 slaveDB에 읽기전용으로 붙어서 쓰고 있다.)

 

 

 

 

 


 

@Async

 

@Async는 spring 에서 제공하는 Thread Pool을 활용하는 비동기 메소드 지원 어노테이션이다. 

@Async로 annotationg된 bean 의 메소드는 별도의 스레드에서 실행

기존 JAVA 에서 비동기 방식으로 메서드를 구현할때  java.util.concurrent.ExecutorService을 활용해서 비동기 방식의 method를 정의 할 때마다, Runnable의 run()을 재구현해야 하는 등 동일한 작업들의 반복이 잦았다.

 

그러나 @Async 사용 시  손쉽게 비동기 메서드 작성 가능

 

spring의 Async 지원은 JDK Proxy 또는 CGlib와 같은 객체를 사용하여 Async가 정의된 객체를 생성

그 후 Spring은 메소드의 로직을 별도의 실행 경로로 제출하기 위해 컨텍스트와 연결된 스레드 풀을 찾으려고 합니다. 구체적으로는, 고유한 TaskExcutor 빈이나 taskExcutor로  이름 지정된 빈을 검색

이러한 빈이 없으면 기본적으로 SimpleAsyncTaskExcutor를 사용

 

SimpleAsyncTaskExcutor는 스레드 풀이 아닙니다. 그렇기 때문에 스레드를 관리하고 재사용하는 것이 아니라 계속 만들어 냅니다.

스레드 자원이 많이 들기 때문에 SimpleAsyncTaskExcutor를 쓰지 말아야 합니다.

 

따라서 TaskExecutor를 빈으로 등록하여 사용하는것이 옮은 방법.

 

아래는 async config 중 일부이다.

@Configuration
@EnableAsync
public class AsyncThreadConfiguration {
	@Bean
	public Executor asyncThreadTaskExecutor() {
		ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
		threadPoolTaskExecutor.setCorePoolSize(5);
		threadPoolTaskExecutor.setMaxPoolSize(10);
		threadPoolTaskExecutor.setThreadNamePrefix("Executor-");
		return threadPoolTaskExecutor;
	}
}

 

 

스레드 생성 하는 경우 

Rules for creating Threads internally by SUN:

  • the number of threads 가 corePoolSize보다 작으면 새로운 스레드를 생성
  • the number of threads 가 corePoolSize 같거나(또는 크다면), task를 큐에 넣습니다.
  • 큐가 다 찼을 때 the number of threads가 maxPoolSize보다 작으면 새로운 스레드를 생성
  • 큐가  the number of treads가 maxpoolsize보다 같거나(또는 크다면) 해당 task를 reject 한다.(RejectExcutionException)

 


 

 

주의사항

 

1. private 메서드 적용 불가 proxy로 만들 수 없기 떄문에

 

2. self-invocation 해서는 안됨 proxy로 만ㄷ루 서 없기 떄문에

 

3. return 값은 void 혹은 CompleteableFuture<>

 

 

 

 

간략하게나마 트랜잭션과 async 처리에 대해서 정리해보았는데  앞으로는 좀더 알고 사용 알 수 있을것 같다.

728x90
728x90

기존에 작성된 회사 리소스가 통일성 이 너무 떨어지는 관계로 리팩토링을 진행 할려고 한다.

 

 

일단 기존 리소스의 문제점을  파악해보면  크게 3가지 문제를 찾았고 이를 수정하고자 한다.

 

1. 생성자 문제(통일되지 않은 방식)

/** 
*	현재 사용중인 생성하는 방법은 레거시 포함 총 4가지 사용중이다
*	1. 정적 팩토리 메서드
*	2. 자바 빈즈
*	3. 심플 빌더 패턴
*	4. 점층적 생성자 패턴
**/

 

  • 정적 팩토리 메서드 예시
public class CreationTest {
    private String id;
    private String desc;
    private String desc2;
    private String desc3;

    private String desc4;

    private String desc5;

    private String desc6;

    private CreationTest(String id, String desc, String desc2, String desc3, String desc4,
            String desc5,
            String desc6) {
        this.id = id;
        this.desc = desc;
        this.desc2 = desc2;
        this.desc3 = desc3;
        this.desc4 = desc4;
        this.desc5 = desc5;
        this.desc6 = desc6;
    }

    public static CreationTest of(String id, String desc, String desc2, String desc3, String desc4,
            String desc5,
            String desc6) {
        return new CreationTest(id, desc, desc2, desc3
                , desc4, desc5, desc6);

    }

 

 

사용하면서 문제점 

  • 사용 할 수록 파라메터 수에 따라 만드는 것이 부담 되고 추가로 할당 시  해당 파라메터명이 확인이되지만(인텔리제이의 경우 확인 가능) 가독성이 너무 떨어진다.

 

자바 빈즈 예시

 

public class CreationTest {
    private String id;
    private String desc;
    private String desc2;
    private String desc3;

    private String desc4;

    private String desc5;

    private String desc6;

	
    
      public CreationTest(String id, String desc, String desc2, String desc3, String desc4,
            String desc5,
            String desc6) {
        this.id = id;
        this.desc = desc;
        this.desc2 = desc2;
        this.desc3 = desc3;
        this.desc4 = desc4;
        this.desc5 = desc5;
        this.desc6 = desc6;
    }


    public void setId(String id) {
        this.id = id;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public void setDesc2(String desc2) {
        this.desc2 = desc2;
    }

    public void setDesc3(String desc3) {
        this.desc3 = desc3;
    }

    public void setDesc4(String desc4) {
        this.desc4 = desc4;
    }

    public void setDesc5(String desc5) {
        this.desc5 = desc5;
    }

    public void setDesc6(String desc6) {
        this.desc6 = desc6;
    }

    public String getId() {
        return id;
    }

    public String getDesc() {
        return desc;
    }

    public String getDesc2() {
        return desc2;
    }

    public String getDesc3() {
        return desc3;
    }

    public String getDesc4() {
        return desc4;
    }

    public String getDesc5() {
        return desc5;
    }

    public String getDesc6() {
        return desc6;
    }
}

 

사용하면서 문제점

  • 공부할 때 처음 배우게 되는 생성자이며 
  • 객체의 일관성이 보장되지 않고, 불변성 유지가 되지 않는다. 
  • 또 객체를 완성하기위해 메서드를 지속적으로 호출..

 

점층적 생성자 예시

public class CreationTest {
    //필수
    private final String id;
    private final String desc;

    //선택
    private final String desc2;
    private final String desc3;
    private final String desc4;
    private final String desc5;
    private final String desc6;
    
    // 필수 매개변수를 가지는 생성자
    public CreationTest(String id, String desc) {
        this(id, desc , null, null, null, null, null);
    }
    
//    //필수 + 선택
    public CreationTest(String id, String desc, String desc2) {
        this(id, desc , desc2, null, null, null, null);
    }
    
    //ALL
    public CreationTest(String id, String desc, String desc2, String desc3, String desc4,
            String desc5,
            String desc6) {
        this.id = id;
        this.desc = desc;
        this.desc2 = desc2;
        this.desc3 = desc3;
        this.desc4 = desc4;
        this.desc5 = desc5;
        this.desc6 = desc6;
    }

 

사용하면서 문제점

생성자의 수가 많아지면 클래스가 복잡해짐

정적 팩토리 메서드와 동일하게 해당 생성자만 보고 파악이 어려움

 

빌더(심플 빌더패턴) 예시 (실제 구현 시에는 lombok을 통해 구현 예정)

public class CreationTest {

    private String id;
    private String desc;
    private String desc2;
    private String desc3;
    private String desc4;
    private String desc5;
    private String desc6;

    public CreationTest(Builder builder) {
        this.id = builder.id;
        this.desc = builder.desc;
        this.desc2 = builder.desc2;
        this.desc3 = builder.desc3;
        this.desc4 = builder.desc4;
        this.desc5 = builder.desc5;
        this.desc6 = builder.desc6;
    }

    public static class Builder {
        private String id;
        private String desc;
        private String desc2;
        private String desc3;
        private String desc4;
        private String desc5;
        private String desc6;

        // 필수적인 필드 : brand
        public Builder(String id, String desc) {
            this.id = id;
            this.desc = desc;
        }
        public Builder desc2(String desc2) {
            this.desc2 = desc2;
            return this;
        }
        public Builder desc3(String desc3) {
            this.desc3 = desc3;
            return this;
        }
        public Builder desc4(String desc4) {
            this.desc4 = desc4;
            return this;
        }
        public Builder desc5(String desc5) {
            this.desc5 = desc5;
            return this;
        }
        public Builder desc6(String desc6) {
            this.desc6 = desc6;
            return this;
        }

        public CreationTest build() {
            return new CreationTest(this);
        }
    }
}

 

사용하면서 문제점

  • 관리해야할 클래스 코드 가 많아짐

 

 

해결 방안

위와 같이 통일되지 않은 구조로 사용중이라 한가지 패턴을 지정할때  심플 빌더 패턴을 적용할 예정이다.

외부 연동 시에도 많은 파라메터와 계층형 구조로 인해  정정 팩토리 메서드라던지 점층적 생성자 패턴은 파라메터 파악이 
어렵기 떄문에

 

 


2.  디렉토리 구조 

현재 실제 리팩토링 예정인 리소스 구조(해당 리소스는 외부 API 연동을 담당하는 리소스 이다)

 

현재 문제점

root 에서 controller, service,  repository 등 으로  바로 내려가는 구조이다 보니  코드 가독성도 떨어지고,  조잡한 느낌이 많이 들었다.

 

 

해결 방안 

2가지 안으로 생각중이다.

1안

 

이 1안의 경우 실제로  외부 연동 해서 데이터를 처리할때 도메인 단위로 분리하여 

재고 처리 , 주문 처리, 배송 추적 처리를 분리하는 방안이다.

 

 

2안

 

 

내가 원하는 방향은 2안 처럼 업체별인 아닌 주문, 배송 추적, 재고 관리 도메인 영역별로 변경을 하고 싶지만, 이미 이전에 쿠팡이라는 업체로 묶은 부분이 있다보니  내 의견대로 진행이 될지는 모르곘다.

 

기존 보다는  1안, 2안 모두 개선은 가능하다보니 시간이 조금 부족하다면

 


3. 서비스 순환 참조 문제

 

정확하게는 일어날 여지가 있다보니  리팩토링 진행하는 과정에서 수정을 할려고 한다. 

 

2번 디렉토리 구조를 완성하면  해당 정면이 되는 파사드 인터페이스 를 통해   대표 서비스 클래스 

하위로  해당 로직 서비스를 붙이고  로직 서비스 끼리는 서로 서비스를 주입받지 않고 사용하는 방향으로 생각중이다.

 

글로만 설명이 어려우니

draw io 를 통해 간략하게 그려봤다.

 

 

클라이언트 에서 호출하여 앱서비스에서 하위 로직 서비스를 개별로 호출하도록 처리하며  하위 service1,2,3 의 경우  서비스를 참조하지 않도록 만들어야한다.


 

4. 테스트코드 

현재 작성된 테스트 코드들  또한 방식이 일정하지 않고 필요한 부분들이 작성되지 않은 부분들이 있어 이런 부분들 또한 코드 및 패키지 리팩토링에 따라 수정을 진행할 예정이다.

(테스트 코드는 이전에 읽었던 책을 다시 정리하여 재정리 예정)

728x90

'코드 > dev' 카테고리의 다른 글

sync vs async, blocking vs non-blocking 차이  (0) 2024.04.13
@async와 @transactional  (0) 2024.04.01
spring Rest Docs  (0) 2024.03.03
Spring boot MongoDB transaction  (0) 2024.02.28
traceId 로그 생성  (0) 2024.02.28
728x90

현재 스웨거 문서로만 적용이 되어 있는데 아무래도 문서 명세서로 보기에는 restdocs이 편할 듯하여 내가 주로 담당하는 리소스에 적용할려고 한다.

일단 스웨거와 restDocs 의 차이

 

  • Spring Rest Dcos
    • 장점
      • 실제 코드에 영향이 없음
      • 테스트코드 성공시에만 문서작성이 완료됨.
    • 단점
      • 적용하기 어려움
  • Swagger
    • 장점
      • API 테스트화면 제공
      • 적용이 매우 쉬운편
    • 단점
      • 실제 코드(프로덕션)에 어노테이션 추가
      • 실제코드와 동기화 안될 수 있음

 

와 같은 특징이 있지만 실제로 써본 결과 프론트 엔드 쪽 작업자가 있는 경우 스웨거가 있는것이 좀더 편하다고 하는사람이 많았다.
그러나 퇴사자 혹은 입사자가 많이 생기는 경우 restDoc 문서를 만들어 두는것이  파악하기에는 훨씬 좋다고 느껴져
2가지 다 적용이 되면 좋을 것 같다.

 

 

 

restDocs 작성 방법

작성에 앞서 몇까지 선택 해야하는 것들이 있는데
1. AsciiDoc VS Markdown => 일단 가이드 문서에 있는 adoc을 사용 했는데 markdown도 사용이 가능 한걸로 확인했다.
장점은 작성이 쉽다는데 공식문서를 따르기로 했다.

2. MockMvc(@WebMvcTest) VS Rest Assured(@SpringBootTest) => 문서 작성시 Mocking을 사용하여 작성
Rest Assured 는 BDD 스타일로 직관적이지만 별도의 구성없이는 @SpringBootTest 로 수행
전체 컨테스트를 로드하여 빈을 주입하기에 속도가 많이 느림.
MockMvc 는 @WebMvcTest 수행  controller Layer만 테스트하여 속도가 빠름 
단순 컨트롤러 테스트만 할 경우 MockMvc가 좋고 아닌 경우 는 Rest Assured가 좋음

JUNIT 외에  Spock라는게 있는데 작업을 할 프로젝트에는 Jnut 이기때문에 고려하지 않기로 한다.

 

 

적용 Project 버전 확인

Sprting boot  2.6.7
java 17
gradle 7

 

 

build.gradle 설정 추가

실제 적용된 코드 

id "org.asciidoctor.jvm.convert" version "3.3.2" //asciidoc 파일 변환 및  복사 플러그인 

configurations {
    asciidoctorExt 
}

ext {// snippets 파일이 저장될 경로 변수 설정
    snippetsDir = file('build/generated-snippets')
}


//dependency 추가
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.6.RELEASE'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.6.RELEASE'

test {// 테스트 실행 시 snippets를 저장할 경로로 snippetsDir 사용
	outputs.dir snippetsDir
}

// API 문서 생성
asciidoctor {
    dependsOn test
    inputs.dir snippetsDir
    attributes 'snippets': snippetsDir
}

tasks.register('copyDocument', Copy) {
    dependsOn asciidoctor
    from file("build/docs/asciidoc")
    into file("src/main/resources/static/docs")
}

build {
    dependsOn copyDocument
}
bootJar {
    dependsOn asciidoctor
    copy {
        from "${asciidoctor.outputDir}"
        into 'src/main/resources/static/docs'
    }

}

 

 

TestCode 작성

//실제 작성된 프로젝트 적용됨으로 컨트롤러와DTO 등 추가로 작성이 필요함

@AutoConfigureMockMvc // -> webAppContextSetup(webApplicationContext)
@AutoConfigureRestDocs // -> apply(documentationConfiguration(restDocumentation))
@SpringBootTest // @WebMvcTest(NaverOrderController.class) 사용하여도 무관
class NaverOrderControllerTest {
		@Autowired
    private MockMvc mockMvc;
		@MockBean
    private NaverOrderService naverOrderService;
		
		@Test
    @DisplayName("메뉴얼 네이버 상세 호출 RestDoc")
    void callManualNaverProductOrderChangedRestDoc() throws Exception {
			mockMvc.perform(patch("/api/partner/v1/naver/order/hope-delivery")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(arrivalDateRequest))
                        .accept(MediaType.APPLICATION_JSON)
                )
                .andDo(print())
                .andExpect(status().isOk())
                .andDo(document("hopeDeliveryRequest"
                        , preprocessRequest(prettyPrint())
                        , preprocessResponse(prettyPrint())
                        , requestFields(
                                fieldWithPath("data").type(JsonFieldType.ARRAY).description("data List")
                                , fieldWithPath("data[].orderSrl").type(JsonFieldType.STRING).description("의뢰번호")
                                , fieldWithPath("data[].productOrderId").type(JsonFieldType.STRING).description("상품주문번호")
                                , fieldWithPath("data[].hopeDeliveryYmd").type(JsonFieldType.STRING).description("희망일자")
                                , fieldWithPath("data[].hopDeliveryHm").type(JsonFieldType.STRING).description("희망시간").ignored()
                                , fieldWithPath("data[].region").type(JsonFieldType.STRING).description("지역").ignored()
                                , fieldWithPath("data[].changeReason").type(JsonFieldType.STRING).description("변경사유")
                                , fieldWithPath("data[].accountId").type(JsonFieldType.STRING).description("판매사계정")
                                )
                        , responseFields(
                                fieldWithPath("successList").type(JsonFieldType.ARRAY).description("success data")
                                , fieldWithPath("[]").description("성공주문번호").optional().type(JsonFieldType.STRING)
                                , fieldWithPath("failList").type(JsonFieldType.ARRAY).description("fail data")
                                ,fieldWithPath("failList[].productOrderId").type(JsonFieldType.STRING).description("실패 상품 주문 번호")
                                ,fieldWithPath("failList[].code").type(JsonFieldType.STRING).description("실패 코드")
                                ,fieldWithPath("failList[].desc").type(JsonFieldType.STRING).description("실패 설명")
                                )
                        )
                );
		}

//파라메터와 response 객체 및 호출 mocking이 필요함

}

 

위 테스트코드 실행 시 아래와 같은 파일들이 생성된다.

 

이후 해당 파일을 추출해서 만들려면 adoc 파일이 필요하다.

 

 

참고문서

https://spring.io/guides/gs/testing-restdocs/

728x90

+ Recent posts