Backend/Spring

Spring Boot Tips, Tricks and Techniques(스프링 부트 팁, 트릭, 기술)

Seyun(Marco) 2021. 9. 21. 14:18
728x90

Spring Boot Tips, Tricks and Techniques

Tip 1. 테스트에서 랜덤 HTTP 포트 사용하기

스프링 부트 테스트에서 정적 포트를 사용하면 안됩니다. 특정 테스트에 대해 @SpringBootTest 에서 webEnviroment 필드를 사용합니다. 기본값은 DEFINED_PORT 로 제공하는데 이 옵션 대신 RANDOM_PORT 를 사용하세요. 그 이후에 @LocalServerPort 를 사용해 테스트에 포트 번호를 삽입할 수 있습니다.

몇 가지 Spring Boot 테스트 팁부터 시작하겠습니다. Spring Boot 테스트에서 정적 포트를 사용하면 안 됩니다. 특정 테스트에 대해 이 옵션을 설정하려면 의 webEnvironment필드 를 사용해야 합니다 @SpringBootTest. 따라서 기본값 대신 값을 DEFINED_PORT제공하십시오 RANDOM_PORT. 그런 다음 @LocalServerPort주석 을 사용하여 테스트에 포트 번호를 삽입할 수 있습니다 .

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AppTest {

   @LocalServerPort
   private int port;

   @Test
   void test() {
      Assertions.assertTrue(port > 0);
   }
}

Tip 2. JPA 계층 테스트에 @DataJpaTest 를 사용하기

일반적으로 @SpringBootTest 인 경우 통합테스트에 사용합니다. 통합테스트의 문제는 전체 애플리케이션 컨텍스트를 실행한다는 점입니다. 이에 따라 테스트를 실행하는 데 필요한 총 시간이 늘어납니다. JPA 컴포넌트를 테스트를 하려고 하면 @DataJpaTest@Respository 빈을 사용하는것이 좋습니다. 기본적으로 SQL 쿼리를 기록하게 되는데 showSql 필드를 비활성화 하면 기록하지 않습니다. @Import 를 사용하면 @Service , @Component 도 사용할 수 있습니다.

@DataJpaTest(showSql = false)
@Import(TipService.class)
public class TipsControllerTest {

    @Autowired
    private TipService tipService;

    @Test
    void testFindAll() {
        List<Tip> tips = tipService.findAll();
        Assertions.assertEquals(3, tips.size());
    }
}

애플리케이션에 여러 통합 테스트가 있는 경우 테스트 어노테이션을 변경할때 주의하세요. 변경을 하게 되면 애플리케이션 컨텍스트의 전역 상태를 수정하므로 테스트 간에 해당 컨텍스트를 재사용하지 않을수도 있습니다.

Tip 3. 각 테스트 후 트랜잭션 롤백하기

일반적으로 테스트 수행 이후 모든 변경사항을 롤백하는 것이 좋습니다. 특정 테스트 중 변경사항을 다른 테스트 결과에 영향을 미치지 않아야 하기 때문입니다. 그러나 이럴때 수동으로 롤백하지 않는것이 좋습니다.

 @Test
 @Order(1)
 public void testAdd() {
     Tip tip = tipRepository.save(new Tip(null, "Tip1", "Desc1"));
     Assertions.assertNotNull(tip);
     tipRepository.deleteById(tip.getId());
 }

이에 따라 스프링부트는 테스트 클래스에 @Transactional 을 붙이면 기본적으로 롤백을 해줍니다. 그러나 클라이언트 측에서만 제대로 작동하며 애플리케이션이 서버 측에서 트랜잭션을 수행하는 경우 롤백되지 않습니다.

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Transactional
public class TipsRepositoryTest {

    @Autowired
    private TipRepository tipRepository;

    @Test
    @Order(1)
    public void testAdd() {
        Tip tip = tipRepository.save(new Tip(null, "Tip1", "Desc1"));
        Assertions.assertNotNull(tip);
    }

    @Test
    @Order(2)
    public void testFindAll() {
        Iterable<Tip> tips = tipRepository.findAll();
        Assertions.assertEquals(0, ((List<Tip>) tips).size());
    }
}

현재는 인메모리 임베디드 데이터베이스를 사용할때의 팁이였지만 복잡한 데이터 구조가 있는 경우 테스트가 실패하면 디버깅하는 대신 커밋된 데이터를 확인해야 할 때가 있습니다. 따라서 외부 데이터베이스를 사용하고 각 테스트 후에 데이터를 커밋해야 합니다. 아울러 테스트를 시작할때 매번 정리하는 방법도 있습니다.

Tip 4. 논리적 "OR"이 있는 다중 스프링 조건

@Conditional 빈에서 여러 조건을 정의하려면 기본적으로 "AND"를 사용합니다. 아래 보이는 경우가 그런 경우 입니다.

@Bean
@ConditionalOnProperty("multipleBeans.enabled")
@ConditionalOnBean({MyBean1.class, MyBean2.class})
public MyBean myBean() {
   return new MyBean();
}

그렇다면 OR 조건을 정의하려면 어떻게 해야 하는가? extends 클래스를 만들고 AnyNestedContion 을 상속받으면 됩니다. 그런 다음 @Conditional 어노테이션을 함께 사용하면 됩니다.

public class MyBeansOrPropertyCondition extends AnyNestedCondition {

    public MyBeansOrPropertyCondition() {
        super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnBean(MyBean1.class)
    static class MyBean1ExistsCondition {}

    @ConditionalOnBean(MyBean2.class)
    static class MyBean2ExistsCondition {}

    @ConditionalOnProperty("multipleBeans.enabled")
    static class MultipleBeansPropertyExists {}
}

@Bean
@Conditional(MyBeansOrPropertyCondition.class)
public MyBean myBean() {
   return new MyBean();
}

Tip 5. Maven 데이터를 애플리케이션에 주입

Maven 데이터를 애플리케이션에 주입할 수 있는 방법은 파일에 project 접두사를 사용하거나 @ 구분 기호가 있는 특수 자리 표시자를 사용하면 됩니다.

  • application.properties
maven.app=@project.artifactId@:@project.version@

그런 다음 @Value을 사용하여 속성을 애플리케이션에 주입하기만 하면 됩니다.

@SpringBootApplication
public class TipsApp {
   @Value("${maven.app}")
   private String name;
}

한편, BuildProperties같이 bean을 사용할 수 있습니다. build-info.properties에서 사용 가능한 데이터를 저장 합니다.

@SpringBootApplication
public class TipsApp {

   @Autowired
   private BuildProperties buildProperties;

   @PostConstruct
   void init() {
      log.info("Maven properties: {}, {}",
         buildProperties.getArtifact(),
         buildProperties.getVersion());
   }
}

Spring Boot Maven Plugin에서 제공하는 build-info 를 실행해build-info.properties 생성할 수 있습니다.

$ mvn package spring-boot:build-info

Tip 6. Git 데이터를 애플리케이션에 주입

때로는 Spring Boot 애플리케이션 내부의 Git 데이터에 접근하고 싶을 수 있습니다. 그렇게 하려면 먼저 git-commit-id-pluginMaven 플러그인을 사용하면 됩니다. 빌드하는 동안 git.properties파일 을 생성 합니다.

<plugin>
   <groupId>pl.project13.maven</groupId>
   <artifactId>git-commit-id-plugin</artifactId>
   <configuration>
      <failOnNoGitDirectory>false</failOnNoGitDirectory>
   </configuration>
</plugin>

git.properties의 내용을 GitProperites 빈으로 주입받을 수 있습니다.

@SpringBootApplication
public class TipsApp {

   @Autowired
   private GitProperties gitProperties;

   @PostConstruct
   void init() {
      log.info("Git properties: {}, {}",
         gitProperties.getCommitId(),
         gitProperties.getCommitTime());
   }
}

Tip 7. 초기 non-production 데이터 삽입

때때로 데모 목적으로 응용 프로그램 시작에 일부 데이터를 삽입해야 할 떄가 있습니다. 이러한 초기 데이터 세트를 사용해 개발 중에 애플리케이션을 수동으로 테스트해야 할 때가 있습니다. 이를 위해 data.sql 파일을 사용하면 됩니다. 일반적으로 src/main/resources 에 사용합니다.

insert into tip(title, description) values ('Test1', 'Desc1');
insert into tip(title, description) values ('Test2', 'Desc2');
insert into tip(title, description) values ('Test3', 'Desc3');

아래와 같이 특정 프로파일에서만 기능을 활성화 하는게 좋습니다.

@Profile("demo")
@Component
public class ApplicationStartupListener implements
      ApplicationListener<ApplicationReadyEvent> {

   @Autowired
   private TipRepository repository;

   public void onApplicationEvent(final ApplicationReadyEvent event) {
      repository.save(new Tip("Test1", "Desc1"));
      repository.save(new Tip("Test2", "Desc2"));
      repository.save(new Tip("Test3", "Desc3"));
   }
}

Tip 8. @Value 대신 @configurationPropertis

동일한 접두사를 가진 여러 속성이 있는 경우 @Value 를 사용하면 안됩니다. 대신 @Configuration 을 사용하는게 좋습니다. 이때 생성자 주입을 사용해야 하며 Getter도 필요합니다.

@ConstructorBinding
@ConfigurationProperties("app")
@AllArgsConstructor
@Getter
@ToString
public class TipsAppProperties {
    private final String name;
    private final String version;
}

@SpringBootApplication
public class TipsApp {
    @Autowired
    private TipsAppProperties properties;
}

Tip 9. Spring MVC를 사용한 오류 처리

Spring MVC 예외 처리는 서버 예외를 클라이언트에 보내지 않도록 하는 데 매우 중요합니다. 현재 예외를 처리할 때 두 가지 권장되는 접근 방식이 있습니다. 첫 번째 단계에서는 @ControllerAdvice@ExceptionHandler주석 과 함께 전역 오류 처리기를 사용합니다 .좋은 방법은 애플리케이션에서 발생하는 모든 비즈니스 예외를 처리하고 여기에 HTTP 코드를 할당하는 것입니다. 기본적으로 Spring MVC는 처리되지 않은 예외에 대해 HTTP 500 코드를 반환합니다.

@ControllerAdvice
public class TipNotFoundHandler {
    @ResponseStatus(HttpStatus.NO_CONTENT)
    @ExceptionHandler(NoSuchElementException.class)
    public void handleNotFound() {
    }
}

컨트롤러 메서드 내에서 모든 예외를 로컬로 처리할 수도 있습니다. 이 경우 특정 HTTP 코드와 함께 ResponseStatusException을 throw하면 됩니다.

@GetMapping("/{id}")
public Tip findById(@PathVariable("id") Long id) {
   try {
      return repository.findById(id).orElseThrow();
   } catch (NoSuchElementException e) {
      log.error("Not found", e);
      throw new ResponseStatusException(HttpStatus.NO_CONTENT);
   }
}

Tip 10. 존재하지 않는 설정 파일 무시하기

일반적으로 구성 파일이 없으면 응용 프로그램이 시작되지 않아야 합니다. 특히 속성에 대한 기본값을 설정할 수 있습니다. Spring 애플리케이션의 기본 동작은 구성 파일이 누락된 경우 시작되지 않으므로 변경해야 합니다. spring.config.on-not-found속성을 ignore로 설정합니다.

$ java -jar target/spring-boot-tips.jar \
   --spring.config.additional-location=classpath:/add.properties \
   --spring.config.on-not-found=ignore

시작 실패를 방지하는 또 다른 편리한 솔루션이 있습니다. 아래와 같이 config 파일 위치에 optional키워드를 사용할 수 있습니다 .

$ java -jar target/spring-boot-tips.jar \
   --spring.config.additional-location=optional:classpath:/add.properties

Tip 11. 다양한 수준의 구성

spring.config.location속성을 사용하여 Spring 구성 파일의 기본 위치를 변경할 수 있습니다 . 속성 소스의 우선 순위는 목록에 있는 파일의 순서에 따라 결정됩니다. 이 기능을 사용하면 일반 설정에서 시작하여 가장 애플리케이션별 설정에 이르기까지 다양한 수준의 구성을 정의할 수 있습니다. 따라서 아래에 보이는 내용이 포함된 전역 구성 파일이 있다고 가정해 보겠습니다.

property1=Global property1
property2=Global property2

또한 아래와 같이 애플리케이션별 구성 파일이 있습니다. 여기에는 전역 구성 파일의 속성과 이름이 같은 속성이 포함됩니다.

property1=App specific property1

그리고 그 기능을 검증하는 JUnit 테스트가 있습니다.

@SpringBootTest(properties = {
    "spring.config.location=classpath:/global.properties,classpath:/app.properties"
})
public class TipsAppTest {

    @Value("${property1}")
    private String property1;

    @Value("${property2}")
    private String property2;

    @Test
    void testProperties() {
        Assertions.assertEquals("App specific property1", property1);
        Assertions.assertEquals("Global property2", property2);
    }
}

Tip 12. Kubernetes에 Spring Boot 배포

Dekorate 프로젝트를 사용하면 Kubernetes YAML 매니페스트를 수동으로 만들 필요가 없습니다. 먼저 io.dekorate:kubernetes-spring-starter종속성 을 포함해야 합니다 . 그런 다음 @KubernetesApplication생성된 YAML에 새 매개변수를 추가하거나 기본값을 재정의하는 것과 같은 주석을 사용할 수 있습니다 .

@SpringBootApplication
@KubernetesApplication(replicas = 2,
    envVars = {
       @Env(name = "propertyEnv", value = "Hello from env!"),
       @Env(name = "propertyFromMap", value = "property1", configmap = "sample-configmap")
    },
    expose = true,
    ports = @Port(name = "http", containerPort = 8080),
    labels = @Label(key = "version", value = "v1"))
@JvmOptions(server = true, xmx = 256, gc = GarbageCollector.SerialGC)
public class TipsApp {
    public static void main(String[] args) {
        SpringApplication.run(TipsApp.class, args);
    }
}

그런 다음 Maven 빌드 명령에서 dekorate.builddekorate.deploy매개 변수를 true로 설정해야 합니다 자동으로 매니페스트를 생성하고 Kubernetes에 Spring Boot 애플리케이션을 배포합니다. Kubernetes에 애플리케이션을 배포하기 위해 Skaffold를 사용하면 Dekorate와 쉽게 통합할 수 있습니다.

$ mvn clean install -Ddekorate.build =true -Ddekorate.deploy=true

Tip 13. 임의의 HTTP 포트 생성

server.port속성을 로 설정하면 Spring Boot는 웹 애플리케이션에 임의의 자유 포트를 할당합니다

server.port=0

사용자 정의 사전 정의 범위(예: 8000-8100)에서 임의의 포트를 설정할 수 있습니다. 그러나 생성된 포트가 할당 해제된다는 보장은 없습니다.

server.port=${random.int(8000,8100)}
728x90
728x90