Spring CORS 설정하기

이번 포스팅에서는 CORS에 대해서 간단한 개념을 짚어보고 Spring을 통해서 CORS 정책 설정 방법을 알아보도록 하겠습니다
자세한 개념은 아래 페이지에 정리가 잘 되어 있으니 프론트/백엔드 상관없이 웹개발자분들은 한번씩 읽어 보시는 것을 추천드립니다 🤗
교차 출처 리소스 공유 (CORS) - HTTP | MDN


CORS란?

먼저, 아래 사진의 에러 내용을 살펴보도록 하겠습니다.

http://localhost:3000' -> ‘http:localhost:8080/todo’를 호출하였더니 위와 같은 메세지가 출력되었습니다. 빨간 네모박스 안에 메세지를 보면, CORS 정책에 의해 접근이 막혔다는 메시지가 나와 있습니다. CORS란 무엇일까요?

 

CORS란 ‘Cross-Origin Resource Sharing’의 약자로 ‘교차 출처 리소스 공유’로 번역됩니다. 웹브라우저 에서 다른 출처의 자원을 공유하는 방법입니다.

 


Origin 이란?

Origin은 출처라는 의미로 번역되는데 일반적으로 '동일/교차' 출처라는 의미로 사용됩니다.
여기서 말하는 Origin은 아래와 같이 표현됩니다
https://yhame.tistory.com

  • 프로토콜 : https
  • 호스트 : yhmane.tistory.com
  • 포트 : 443 (80, 443 포트는 생략이 가능합니다 😀)

즉, 여기서 말하는 Origin은 우리가 흔히 얘기하는 URL 구조에서 '프토토콜 + 호스트 + 포트'를 합친것과 같습니다

Same Origin / Cross Origin

위에서 알아본 동일/교차 출처에 대해서 url과 비교하여 동일출처인지 다른출처인지 비교해보도록 하겠습니다. 기준 url은 아래와 같습니다

# 기준 url
https://yhmane.tistory.com
URL 출처 내용
https://yhmane.tistory.com/category 동일출처  protocol, host, port 동일
https://yhmane.tistory.com/198?category=769262 동일출처 protocol, host, port 동일
http://yhmane.tistory.com/category 다른출처 protocal이 다름
https://yhmane.tistory.com:8081/category 다른출처 port가 다름
https://github.com/yhmane 다른출처 host가 다름

※ 동일출처의 경우 SOP (Same-Origin Policy)라 웹브라우저 상에서 보안상 이슈가 없지만, 교차출처의 경우 보안상 이슈로 인해 웹브라우저상에서 에러메시지가 처리됩니다. 결국, 이러한 문제를 해결하기 위해서는 서버(Spring, Node 등등)에서 CORS를 처리해주어야 합니다.


Spring CORS 설정하기

Spring에서는 크게 2가지 방식으로 CORS를 설정할 수 있습니다.

  1. @Configuration 설정 방법
  2. @CrossOrigin 설정 방법

일반적으로는 1번 @Configuration 설정 방법을 많이 사용합니다

@Configuration 이용 방법

먼저, @Configuration을 이용하여 전역으로 처리하는 방법을 알아보겠습니다.

* Java

@Configuration
public class CorsConfig implements WebMvcConfigurer {

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
			.allowedOrigins("http://127.0.0.1:8080")
			.allowedMethods(
				HttpMethod.GET.name,
				HttpMethod.POST.name,
				HttpMethod.PUT.name,
				HttpMethod.DELETE.name
			)
	}
}

* Kotlin

@Configuration
class CorsConfig : WebMvcConfigurer {

	override
	fun addCorsMappings(registry: CorsRegistry) {
		registry.addMapping("/**")
			.allowedOrigins("http://localhost:3000")
			.allowedMethods(
				HttpMethod.GET.name,
				HttpMethod.POST.name,
				HttpMethod.PUT.name,
				HttpMethod.DELETE.name
			) 
	} 
}
  • addMapping
    CORS 적용할 Url 패턴을 정의합니다. 모든 url에 대한 접근을 허용할 경우 위와 같이 ‘/**’로 설정합니다.
  • allowedOrigins
    위에서 Origin은 (Protocol + host + port)의 조합이라고 들었습니다. 따라서, 허용하고자 하는 Origin을 적어주시면 됩니다. 마찬가지로 chaining도 가능합니다
  • allowedMethods
    허용하고자 하는 method를 적어주시면 됩니다.

@CrossOrigin 이용 방법

어노테이션 이용방법은 조금 더 간단합니다.
해당 클래스나 메서드위에 @CrossOrigin 어노테이션을 붙여주면 됩니다.

* Java

@CrossOrigin("http://localhost:3000")
@RestController
@RequestMapping("/todo")
public class TestController {
	
	//@CrossOrigin("http://localhost:3000")
	@GetMapping public String test() {
		// test ..
	}
}

* Kotlin

@CrossOrigin("http://localhost:3000")
@RestController
@RequestMapping("/todo")
class TestController {

	//@CrossOrigin("http://localhost:3000")
	@GetMapping fun test() {
		// test ..
	}
}

정리

  • 웹브라우저를 통해 동일출처가 아닌 교차출처를 이용해야 할 경우 CORS 설정을 해야 합니다
  • Origin은 (프로토콜 + 도메인 + 포트)로 구성됩니다
  • Spring에서 CORS 설정은 전역, @CrossOrigin을 이용해서 설정합니다
  • 이외에도 필터를 이용해 CORS 설정을 해줄수도 있습니다

출처

교차 출처 리소스 공유 (CORS) - HTTP | MDN
Cross Origin Resource Sharing - CORS - 뒤태지존의 끄적거림
baeldung

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Spring Test Mockito, BDDMockito


들어가며

Spring을 이용해 MVC 테스트를 진행하게 되면 한번쯤 Mock을 써보게 될텐데요, Mock을 써보며 대부분 Mockito를 접하게 되리라 생각합니다. 다만, Mockito에는 많은 API가 존재해서 어느 상황에 어떠한 메서드를 사용해야 할지 혼란스러울 때가 있습니다.

이번 포스팅에서는 Mockito의 when(), doXXX()에 대하여 알아보고 BDDMockito와 비교하여 어떠한 메서드를 사용하는게 좋을지 알아보도록 하겠습니다.

더불어 마틴 파울러의 given-when-then 테스트에 대해서도 한번 읽어 보시면 이번 포스팅을 이해하는데 도움이 되리라 생각합니다.


비즈니스 로직

이해를 돕기 위해 간단한 Controller와 Service를 가정하여 봅시다. 등록된 상점 ID를 이용해 상점을 찾는 API입니다.

  • StoreController
    • find()
    • return StoreResponseDto (상점 Dto)
  • StoreService
    • find(id) 상점 단건 조회

when() method

@DisplayName("store_단건_조회")
@Test
void findStore_when() throws Exception {
    // given
    StoreResponseDto storeDto = new StoreResponseDto(StoreEntity.builder()
        .id(1L)
        .name("곱돌이네")
        .telephone("02-1234-5678")
        .address("서울 송파구 송파1로 01")
        .managerName("황윤호")
        .businessNumber("123123933")
        .build());

    when(storeService.find(1L)).thenReturn(storeDto);

    // when, then
    String url = "/stores/1";
    mockMvc.perform(MockMvcRequestBuilders.get(url)
        .accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(content().string(containsString("123123933")));
}

when()을 통해 mocking한 Service 로직을 수행하고, return 값으로 storeDto을 받아 왔습니다. 여기서는 return 값이 있기에 잘 수행됩니다.
하지만, when()은 void() 형식에 대처할 수 없는 문제가 있습니다.

doXXX() method

@DisplayName("store_단건_수정")
@Test
void updateStore_doXXX() throws Exception {
    ...
   doNothing().when(storeService).update(1L, storeRequest);
    ...
}

이러한 when()의 문제는 doXXX와 함께 사용한다면 void return type에 대하여 대처가 가능합니다. (doNothing)

when(storeService.find(1L)).thenReturn(storeDto);
doReturn(store).when(storeService).find(1L);

마찬가지로 조회 역시 위와 같이 대체하여 작성할 수 있습니다.

BDDMockito

@DisplayName("store_단건_수정")
@Test
void updateStore_bdd() throws Exception {
   ...
   willDoNothing().given(storeService).update(1L, storeRequest);
   ...
}

willDoNothing()을 통해 void() 형태에 대처할 수 있습니다.

when(storeService.find(1L)).thenReturn(storeDto);
given(storeService.find(1L)).willReturn(store);

마찬가지로 조회 역시 위와 같이 대체하여 작성할 수 있습니다.


결론

마틴 파울러의 ‘given-when-then’ 테스트 방법론에 대하여 한번쯤은 들어봤을 겁니다.

  1. given - 시나리오에서 구체화
  2. when - 구체적으로 기술하고자 하는 행동
  3. then - 기대하는 변화/값

BDDMockito도 이와 같이 행위 주도 개발(Behaviour-Driven Development)을 따르고 있습니다.

위에서 설명한 예시를 다시 한번 살펴 보도록 하겠습니다.

// given
when(storeService.find(1L)).thenReturn(storeDto); // Mockito
given(storeService.find(1L)).willReturn(store); // BDDMockto

Mockito의 when()을 이용할 경우 given(시나리오 구체화) 단계에서 when으로 명시하는 상황이 발생하게 됩니다.
Mockito의 test가 틀렸다는 것은 아닙니다. 다만, BDDMockito를 이용한다면 좀 더 자연스러운 테스트 코드를 작성할 수 있습니다.

  • given-when-then 테스트 패턴에 어울리는 건 BDDMockito
  • void return type에 대응하기 위해서는 doNothing() 또는 willDoNothing()과 함께 사용

참조

Difference Between when() and doXxx() Methods in Mockito | Baeldung
Mockito와 BDDMockito는 뭐가 다를까?

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며

스프링 프레임워크를 이용하여 웹 개발을 하다 보면 중복으로 처리하는 코드들이 생깁니다. 인증, 인가, 세션, XSS 방어 등 공통적으로 다루게 되는 부분이 생기게 되는데 인터셉터, 필터, AOP 등을 이용하여 각 상황에 맞추어 처리를 하여 줍니다.

이번 포스팅에선 스프링 필터(Filter)를 이용한 공통 부분 제어를 포스팅 하도록 하겠습니다.

 

스프링 필터

필터는 애플리케이션의 HTTP 요청 및 응답을 가로채는 데 사용되는 개체입니다. 필터를 사용하여 두 인스턴스에서 두 가지 작업을 수행 할 수 있습니다.

  • client의 요청을 가로채어 작업을 수행할 수 있습니다.

  • response 되기 전에 가로채어 작업을 수행할 수 있습니다.

    필터는 스프링 컨텍스트 외부에서 request와 response의 해당하는 작업을 가로채어 공통 로직을 수행합니다. 인터셉터와 AOP와 실행되는 시점이 다르니 각 상황에 맞추어 적절히 수행하면 됩니다. (Interceptor의 경우 추후 포스팅 하도록 하겠습니다)

> 간단히 코드로 알아보겠습니다.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestRestController {

    @GetMapping
    public String test() {
        return "hello";
    }
}


import org.springframework.stereotype.Component;

import javax.servlet.*;
import java.io.IOException;

@Component
public class SimpleFilter  implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Remote Host:"+request.getRemoteHost());
        System.out.println("Remote Address:"+request.getRemoteAddr());
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        System.out.println("destory");
    }
}

> 응답결과

Remote Host:127.0.0.1
Remote Address:127.0.0.1

 

 

> 메서드 (implements)

필터의 경우 다음 메서드들을 상황에 맞추어 @Override 해주면 됩니다.

  • public void init(FilterConfig filterConfig) - 필터를 웹 콘테이너에 생성 후 초기화할 때 호출됩니다.
  • public void doFilter(ServletRequest request, SevletResponse response, FilterChain chain) - 필터체인으로 연결하여 줍니다. 체인의 경우 순서를 지정할 수 있습니다. 체인의 가장 마지막에는 클라이언트가 요청한 최종 자원이 위치합니다.
  • public void destroy() - 필터가 웹 콘테이너에서 삭제될 때 호출됩니다.

 

정리

필터는 이처럼 기본 구조와 원리만 이해하면 요긴하게 사용할 수 있습니다. Request/Response의 처리를 chain 하는 것에 대한 이해를 요구하므로 로그인, 권한, 인코딩 등 상황에 맞추어 사용하시면 됩니다.

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며

 

 

 코드를 작성하다 보면 공통적인 기능을 하는 로직이 생깁니다. 이러한 공통 기능들을 모듈에 적용하기 위해 Java 에서는 상속이라는 것을 이용하여 처리하였습니다. 하지만 상속으로 처리하기에는 몇가지 문제가 존재합니다.

 

 먼저, Java는 다중 상속이 가능하지 않기에 다양한 모듈에 공통 기능을 부여하기에 한계가 있습니다. 다음으로, 기능구현 부분에 핵심기능(비즈니스 로직)과 공통 기능이 섞이면 효율성이 떨어집니다. 또한 공통기능을 핵심기능(비즈니스 로직) 등에 계속 사용하다 보면 코드의 중복이 많이 발생하게 됩니다.

 

 이러한 이유로 '관점 질향 프로그래밍' AOP의 필요성이 대두되었고 Spring의 핵심 기능으로 등장하게 되었습니다.

 

 

AOP란?

 

 AOP는 공통기능을 핵심기능과 분리하고, 공통 기능중에서 핵심 기능에 적용하고자 하는 부분에 적용 시키는 것입니다. 다음과 같이 프록시를 두어 공통 기능을 핵심 기능이 수행할 때, 수행시키는 것 입니다.

 

 

  • Aspect : 공통 기능
  • Advice : Aspect의 기능 자체
  • Jointpoint : Advice를 적용해야 되는 부분 ex) 필드, 메서드 [스프링에서는 메서드만 해당]
  • Pointcut : Jointpoint의 부분으로 실제로 Advice가 적용된 부분
  • Weaving : Advice를 핵심 기능에 적용하는 행위

 

AOP 사용 방법

 

pom.xml에 dependency를 추가하여 줍니다

<!-- AOP -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.7.4</version>
</dependency>

 

다음으로 dispatcher를 통해 주입 시켜줍니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
                           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">

    <!--AOP 설정을 통한 자동적인 Proxy 객체 생성을 위한 설정-->
    <aop:aspectj-autoproxy/>

/>

xml 파일에 프록시를 등록하여 주면, 서버가 올라갈 때 Dispatcher가 Annotation을 보고 매핑하여 줍니다.

 

 

마지막으로 method 수행 시간을 측정하는 클래스를 생성하여 보겠습니다.

import java.util.Arrays;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LogAdvice {

    private static final Logger logger = LoggerFactory.getLogger(LogAdvice.class);

    // 범위지정
    @Around("execution(* spring.mvc.sample..*Controller.*(..))"
            + " or execution(* spring.mvc.sample..service..*Impl.*(..))"
            + " or execution(* spring.mvc.sample..persistence..*Impl.*(..))")
    public Object logPrint(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        // 시작시간
        long start    = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        String type   = proceedingJoinPoint.getSignature().getDeclaringTypeName();
        String name   = "";

        // type 종류
        if (type.contains("Controller")) {
            name = "Controller : ";
        } else if (type.contains("Service")) {
            name = "Service : ";
        } else if (type.contains("DAO")) {
            name = "Persistence : ";
        }

        // 완료시간
        long end = System.currentTimeMillis();

        logger.info(name + type + "."+proceedingJoinPoint.getSignature().getName() + "()");
        logger.info("Argument/Parameter : " + Arrays.toString(proceedingJoinPoint.getArgs()));

        // return 값
        if (result != null) {
            logger.info("Return Value : " + result.toString());
        } else {
            logger.info("Return Type : void");
        }

        logger.info("Running Time : " + (end-start));
        logger.info("----------------------------------------------------------------");

        return result;
    }
}

execution을 통해 범위를 지정시킬 수 있습니다.

@Around("execution(* spring.mvc.sample..*Controllrt.*(..))"

+ " or execution(* spring.mvc.sample..service..*Impl.*(..))"

+ " or execution(* spring.mvc.sample..persistence..*Impl.*(..))")

Controller, Service, DAO 부분에 AOP를 설정하였습니다.
다음을 실행하게 되면,type :Parameter :Return Value :Running Time :의 로그가 찍히는 것을 확인할 수 있습니다.

@Around 이외에도 타겟 메서드의 Aspect 실행 지점을 지정할 수 있습니다. 아래 어노테이션을 이용하여 어드바이스를 만들 수 있습니다.

  • @Before : 타겟 메서드가 호출되기 이전에 어드바이스 기능을 수행
  • @After : 타겟 메서드 결과 (성공, 실패)에 관계 없이 어드바이스 기능을 수행
  • @AfterReturning : 타겟 메서드의 결과가 성공할 경우에 어드바이스 기능을 수행
  • @Throws : 타겟 메서드의 결과가 예외를 던질 경우에만 어드바이스 기능을 수행
 

참조

 

- 코드로 배우는 스프링 웹프로젝트

- Spring Doc

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며


스프링은 @ComponentScan은 서버 실행시 @Component 어노테이션 및 streotype(@Controller, @Servicie, @Repository)이 지정된 클래스들을 찾아 Bean으로 등록해주는 역할을 합니다. 우리는 streotype 이외에 @Component, @Configuration 어노테이션을 이용하여 사용자가 생성한 클래스들을 지정하여 등록하여 주는데 이번 포스팅에선 두 어노테이션의 차이를 알아보도록 하겠습니다.



Streotype 흝어보기


Spring은 Component-Scan을 통해서 Bean을 등록하여 줍니다. Streotype에 API를 확인해 보면 다들 @Component 어노테이션이 선언 되어 있는 것을 확인할 수 있습니다.


 @Controller 

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {

/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
*/
@AliasFor(annotation = Component.class)
String value() default "";

}

@Repository

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {

/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
*/
@AliasFor(annotation = Component.class)
String value() default "";

}

@Service

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {

/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
*/
@AliasFor(annotation = Component.class)
String value() default "";

}



Configuration의 API를 확인해보면 Streotype과 마찬가지로 @Component가 선언되어 있는 것을 확인할 수 있습니다.

 

@Configuration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {

/**
* Explicitly specify the name of the Spring bean definition associated with the
* {@code @Configuration} class. If left unspecified (the common case), a bean
* name will be automatically generated.
* <p>The custom name applies only if the {@code @Configuration} class is picked
* up via component scanning or supplied directly to an
* {@link AnnotationConfigApplicationContext}. If the {@code @Configuration} class
* is registered as a traditional XML bean definition, the name/id of the bean
* element will take precedence.
* @return the explicit component name, if any (or empty String otherwise)
* @see AnnotationBeanNameGenerator
*/
@AliasFor(annotation = Component.class)
String value() default "";

/**
* Specify whether {@code @Bean} methods should get proxied in order to enforce
* bean lifecycle behavior, e.g. to return shared singleton bean instances even
* in case of direct {@code @Bean} method calls in user code. This feature
* requires method interception, implemented through a runtime-generated CGLIB
* subclass which comes with limitations such as the configuration class and
* its methods not being allowed to declare {@code final}.
* <p>The default is {@code true}, allowing for 'inter-bean references' via direct
* method calls within the configuration class as well as for external calls to
* this configuration's {@code @Bean} methods, e.g. from another configuration class.
* If this is not needed since each of this particular configuration's {@code @Bean}
* methods is self-contained and designed as a plain factory method for container use,
* switch this flag to {@code false} in order to avoid CGLIB subclass processing.
* <p>Turning off bean method interception effectively processes {@code @Bean}
* methods individually like when declared on non-{@code @Configuration} classes,
* a.k.a. "@Bean Lite Mode" (see {@link Bean @Bean's javadoc}). It is therefore
* behaviorally equivalent to removing the {@code @Configuration} stereotype.
* @since 5.2
*/
boolean proxyBeanMethods() default true;

}


 즉 @Component, @Configuration, @Controller, @RestController, @Service, @Repository 어노테이션은 서버 실행시 Component-Scan을 통해 Bean이 등록되는 것을 API 문서를 통해 확인할 수 있었습니다. 


@Component, @Configuration 차이


@Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.formLogin().disable() // 로그인 폼 해제
.headers().frameOptions().disable();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}


@Component


@Component
public class FileUtils {

public int createDirectory(String path){

File dir = new File(path);
int cnt = 0;

if (!dir.exists()) {
try{
dir.mkdir();
cnt++;
} catch(Exception e){
e.getStackTrace();
}
}

return cnt;
}

public int createFile(MultipartFile file, String path){

int cnt = 0;
if (!file.isEmpty()){
try {
file.transferTo(new File(path + file.getOriginalFilename()));
cnt++;
} catch (IOException e) {
e.printStackTrace();
}
}
return cnt;
}
}



@Component 와 @Configuration은 큰 차이는 없습니다. 

위에서 보앗듯이, @Configuration의 내부를 살펴보면 @Component가 정의되어 있습니다. 따라서 @Component와 @Bean을 비교하는 것이 맞습니다.


우선적으로, 개발자가 직접 작성한 클래스에 대하여 @Component는 위와 같이 bean으로 등록 할 수 있습니다. 반대로 라이브러리 혹은 내장 클래스등 개발자가 직접 제어가 불가능한 클래스의 경우 @Configuration + @Bean 어노테이션을 이용하여 bean으로 등록하여 사용하면 됩니다.



정리

 

 @Component

 - 개발자가 직접 작성한 클래스를 bean 등록하고자 할 경우 사용


 @Configuration + @Bean

 - 외부라이브러 또는 내장 클래스를 bean으로 등록하고자 할 경우 사용. 

 - 1개 이상의 @Bean을 제공하는 클래스의 경우 반드시 @Configuraton을 명시


블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며

 

  스프링 프레임워크란 자바 플랫폼을 위한 오픈소스 애플리케이션 프레임워크로 간단히 스프링(Spring)이라고도 합니다. 한국에서는 전자정부 프레임워크의 영향을 받아서인지 압도적인 점유을 보이고 있습니다. 스프링을 사용하면 여러 장점들이 있고 그래서인지 개발자들의 많은 선택을 받아 왔습니다. 이번 포스팅에선, 우리가 선택하고 있는 스프링 프레임워크에 뼈대가 되는 3대 요소 (IOC/DI, AOP, 서비스추상화) 에 대하여 글을 작성하도록 하겠습니다.

 

 

 

제어의 역전 (Inversion of Control, IoC)

 

 Spring을 처음 접하게 되면 Servlet 컨테이너, EJB 컨테이너, IoC 컨테이너 등 Container라는 단어를 많이 듣게 됩니다.  컨테이너의 사전적 의미를 찾아보면 '화물을 능률적이고 경제적으로 수송하기 위해 사용하는 상자형 용기'를 의미합니다. 그렇다면 Spring에서 말하는 컨테이너란 무엇일까요? 마찬가지로 객체들을 담는 용기로 사용되어 지는데, Spring Container는 Spring Bean들의 생명주기를 관리합니다. 빈의 생성과 관계, 사용, 생명 주기를 관리하고 컨테이너를 통해 등록된 빈들은 시스템에 전반적으로 사용 가능합니다. 

 

 

 

 

 Spring Container는 어플리케이션을 구성하는 Bean들을 관리하기 위해 IoC(Inversion of Control, 제어의 역전)라는 개념을 이용하였습니다. 아래에 왼쪽 그림은 기존의 객체 생성방식입니다. 클래스 내부에 new 연산자를 이용하여 객체를 생성하였고 생명주기를 관리하였습니다. Spring에서는 오른쪽 그림의 방식으로 객체를 생성합니다. 외부에서 객체를 생성하고 생성자/수정자 방식을 이용하여 객체를 주입하는 방식을 채택하였습니다. 아 방식을 제어하는 주체가 역전되었다 하여 '제어의 역전(IoC)'라 부르며 스프링의 3대 핵심 요소입니다. 

 

 

 

 

 

 즉 IoC란 인스턴스의 생성부터 소멸까지의 인스턴스의 생명주기 관리를 컨테이너가 대신 해주는다는 것입니다.

 

 

의존성 주입 (Dependency Injection, DI)

 

 Spring DI란, IoC 컨테이너에서 빈객체를 생성하는 방식을 말합니다. 위 그림에서와 같이 기존 new로 생성하는 것을 외부에서 주입하는 방식으로 Spring에서는 Bean 생명주기를 관리하여 줍니다. 즉, IoC와 DI는 밀접한 관련이 있고 컨테이너에서 생명주기를 관리하여 준다는 것입니다. 이러한 DI는 3가지 의존성 주입 방식이 있습니다.

 

* 필드 주입

 

@RestController
public class BookController {
    
    @Autowired
    private BookService bookService;    
}

- 가장 일반적이고 쉬운 DI 방법

- final로 지정할 수 없기에 mutable 하여 NullPointerException이 발생할 수 있습니다.

- Runtime에서 예기치 않은 순환참조 문제가 발생할 수 있습니다.

 

* 수정자 주입

 

@RestController
public class BookController {

    private BookService bookService;

    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }
}

- 생성당시 주입을 꼭 하지 않아도 됩니다. 즉, 원하는 호출 타이밍에 함수를 호출하여 주입시킬 수 있습니다.

- 마찬가지로 mutable하여 NullPointerException이 발생할 수 있습니다.

 

 

* 생성자 주입

@RestController
public class BookController {

    private final BookService bookService;
    
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }
}

- Pivotal에서 가장 권장하는 DI 방식입니다.

- final 지정이 가능하기에 Immutable 합니다.

- 서버 실행시 순환참조로 인해 오류가 발생합니다. 즉 Runtime에서 순환참조가 발생하지 않습니다.

- 테스트 코드 작성이 유리합니다.

 

 

정리

 

 

 Spring의 3대 핵심 요소인 IoC/DI에 대하여 학습하였습니다. 서로 밀접한 상관 관계가 있고 Bean의 사이클을 관리하기에 Spring이 취하고 있는 Bean 생성/소멸 방식을 이해하는 것이 좋습니다. Bean 주입 방법으로는 3가지가 존재하는데, 가능하면 Spring 입문시 배우는 필드 주입 방식보식 보다 생성자 주입 방식으로 코드를 작성하는 것이 안전한 코드를 작성하는 방법입니다.

 

 

 

블로그 이미지

사용자 yhmane

댓글을 달아 주세요


처음배우는 스프링부트2의 

내용을 일부 요약하여 정리한 포스팅입니다.


@SpringBootTest


@SpringBootTest는 통합 테스트를 제공하는 기본적인 스프링 부트 어노테이션. 

실제 애플리케이션과 똑같이 애플리케이션 컨텍스트를 로드. 또한, 설정된 빈을 모두 load하기 때문에 규모가 클수록 느려짐

통합 테스트에 적합한 어노테이션


@RunWith(SpringRunner.class)를 붙여서 SpringJUnit4ClassRunner를 상속 받아 사용


@RunWith(SpringRunner.class)
@SpringBootTest(value = "value=test", classes = {TestApplicationTests.class})
public class TestApplicationTests {

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

@Test
public void contextLoads() {
assertThat(value, is("test"));
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {"property.value=propertyTest"}, classes = {TestApplicationPropertyTests.class})
public class TestApplicationPropertyTests {

@Value("${property.value}")
private String propertyValue;

@Test
public void isEqualPropertyValue() {
assertThat(propertyValue, is("propertyTest"));
}

}

- value : 테스트가 실행되기 전에 적용할 프로퍼티를 주입.

- properties: 테스트가 실행되기 전에 {key=value} 형식으로 프로퍼티를 추가



@WebMvcTest

@WebMvcTest는 웹에서 테스트 하기 힘든 컨트롤러를 테스트하는 데 적합.(테스트하고자 하는 특정 컨트롤러명을 명시해 두어야 함)

웹상에서 응답과 요청에 대한 테스트를 진행.

- MockMvc는 컨트롤러 테스트 시, 모든 의존성을 로드하는 것이 아닌 명시된(BookController) 클래스 관련 빈만 로드하여 가벼운 MVC 테스트를 수행

=> 전체 HTTP 서버를 실행하지 않고 테스트를 할 수 있음


@RunWith(SpringRunner.class)
@WebMvcTest(BookController.class)
public class BookControllerTest {

@Autowired
private MockMvc mvc;

@MockBean
private BookService bookService;

@Test
public void Book_MVC_테스트() throws Exception {
Book book = new Book("Spring Boot Book", LocalDateTime.now());
given(bookService.getBookList()).willReturn(Collections.singletonList(book));

mvc.perform(get("/books"))
.andExpect(status().isOk()) // status? 200
.andExpect(view().name("book")) // return view? book
.andExpect(model().attributeExists("bookList")) // model property exist? bookList
.andExpect(model().attribute("bookList", contains(book))); // bookList contains? book
}
}


@service의 경우, @WebMvcTest의 적용 대상이 아니기 때문에 @MockBean으로 객체를 대체


check

- status ? 200

- viewName ? book

- model property Exist? bookList

- bookList contain ? book


@DataJpaTest


@DataJpaTest는 JPA 관련된 테스트 설정만 로드

기본적으로 인 메모리 데이터베이스를 사용(h2)

테스트가 끝날때마다, 테스트에 사용된 데이터를 롤백하여 줌


@RunWith(SpringRunner.class)
@DataJpaTest
public class BookJpaTest {
private final static String BOOT_TEST_TITLE = "Spring Boot Test Book";

@Autowired
private TestEntityManager testEntityManager;

@Autowired
private BookRepository bookRepository;

@Test
public void Book_저장하기_테스트() {
Book book = Book.builder().title(BOOT_TEST_TITLE).
publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book);
assertThat(bookRepository.getOne(book.getIdx()), is(book));
}

@Test
public void BookList_저장하고_검색_테스트() {
Book book1 = Book.builder().title(BOOT_TEST_TITLE).
publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book1);
Book book2 = Book.builder().title(BOOT_TEST_TITLE+"2").
publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book2);
Book book3 = Book.builder().title(BOOT_TEST_TITLE+"3").
publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book3);

List<Book> bookList = bookRepository.findAll();
assertThat(bookList, hasSize(3));
assertThat(bookList, contains(book1, book2, book3));
}
}


@DataJpaTest 사용할 경우에 TestEntityManager 클래스가 자동으로 빈으로 등록 됨. 

위와 같이 TestEntityManager 주입받아서 사용하면 된다. Jpa EntityManager와는 다르게 메서드가 많이 없지만 테스트하기에는 불편함이 없다.


만약, 인메모리 데이터베이스가 아닌 실메모리 데이터베이스를 사용하고자 한다면 아래와 같이 설정하여 주면 된다.

Replace,NONE으로 설정하면 @ActiveProfiles에서 설정한 프로파일이 적용 됨.


@ActiveProfiles("sample")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)



@RestClientTest


@RestClientTest는 Rest 관련 테스트를 도와주는 어노테이션

Rest 통신의 데이터형으로 사용되는 JSON 형식을 테스트


@RunWith(SpringRunner.class)
@RestClientTest(BookRestService.class)
public class BookRestTest {

@Autowired
private BookRestService bookRestService;

@Autowired
private MockRestServiceServer server;

@Test
public void rest_테스트() {
this.server.expect(requestTo("/rest/test"))
.andRespond(withSuccess(new ClassPathResource("/test.json",
getClass()), MediaType.APPLICATION_JSON));
Book book = this.bookRestService.getRestBook();
assertThat(book.getTitle()).isEqualTo("테스트");
}
}

@RestClinentTest는 대상이 되는 BookRestService를 객체로 주입

MockRestServiceServer는 지정된 경로에 반환되는 예상값을 명시해 줌


@JsonTest


@JsonTest는 JSON의 직렬화와 역직렬화를 수행하는 라이브러리인 Gson과 Jackson API의 테스트를 제공


@RunWith(SpringRunner.class)
@JsonTest
public class BookJsonTest {

@Autowired
private JacksonTester<Book> json;

@Test
public void json_테스트() throws Exception {
Book book = Book.builder()
.title("테스트")
.build();
String content = "{\"title\":\"테스트\"}";

assertThat(this.json.parseObject(content).getTitle()).isEqualTo(book.getTitle());
assertThat(this.json.parseObject(content).getPublishedAt()).isNull();

assertThat(this.json.write(book)).isEqualToJson("/test.json");
assertThat(this.json.write(book)).hasJsonPathStringValue("title");
assertThat(this.json.write(book)).extractingJsonPathStringValue("title")
.isEqualTo("테스트");
}
}

- JacksonTester의 parseObject를 통해 content를 객체로 변환.


[출처]


처음으로배우는 스프링부트2

블로그 이미지

사용자 yhmane

댓글을 달아 주세요


@Controller @RestController의 차이 HTTP Response Body가 생성되는 방식의 차이


@Controller를 사용하는 경우

  1. Client가 URI 요청을 보냄
  2. DispatcherServlet과 Handler Mapping이 요청을 Intercept
  3. Controller에 의해 요청을 처리 하고 DispatcherServlet Model과 View를 적절히 Client에 리턴
@Controller
public class WebController {

@GetMapping("/")
public String main(Model model) {
return "main";
}
}


@ResponseBody를 사용할 경우

Spring3 버전 이후로 출시

자바 객체를 HTTP 요청의 body 내용으로 매핑하는 역할을

 1.2 방식은  @Controller와 같다

 3. 여기에서는 View를 거치지 않고 Controller에서 직접 데이터를 리턴


@Controller
public class WebController {

@PostMapping("/post")
public @ResponseBody Posts hello(@RequestBody Posts post) {
// post.set함수
// post.set함수
// post.set함수
return post;
}
}


[추가]

@RequestBody 어노테이션이란?

HTTP 요청의 body 내용을 자바 객체로 매핑하는 역할을 합니다.

-> 당연히 method 방식으로 Get을 쓰면 원하는 인자값을 받지 못함

        -> Get은 Body데 데이터를 담지 않기 때문에, POST를 사용


추가적으로 @PathVariable, @RequestParam이 있다

자꾸 딴곳으로 새는데 ,,, 이 부분도 같이 보는것이 좋다.


@PathVariable  

URI 템플릿 변수에 접근할 때 사용


@RequestParam  

Http 요청 파라미터를 매핑


@GetMapping("/board/{boardNo}")
public String hello(@RequestParam String name, @RequestParam String birth, @PathVariable String boardNo) {
return "Hello World";
}

들어온 데이터 값을 따로 처리하지는 않았지만, 위와 같은 방식으로 사용된다.

@RequestParam은 String name = request.getParameter("name")과 같다.



@RestController 어노테이션이란?

@RestController = @Controller + @ResponseBody 

Spring 4 버전이후로 출시

@Controller와 @ResponseBody를 @RestController가 가지고 있기 때문에 데이터 중심의 구현

작동 방식은 @ResponseBody와 동일


@RestController
public class WebRestController {

@GetMapping
public String hello() {
return "Hello World";
}
}


@RestController를 자세히 본다면, @Controller와 @ResponseBody 를 확인할 수 있음

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {




[참조]

https://docs.spring.io/spring/docs/current/javadoc-api/




블로그 이미지

사용자 yhmane

댓글을 달아 주세요

하나의 어플리케이션을 배포하면서

빠르게 개발하고, 테스트를 수행하고 오류 없이 실서버에 배포를 해주어야 합니다.


다소 번거로운 작업이지만, 

개인 local에서 수행할 환경, 테스트나 dev 환경 다룰 환경, 실서버에서 다룰 영역을 다루게 구분해 주고

쉽게 변경이 가능해야 능률이 올라갑니다.



이러한 작업을 yml 파일을 통해 구축할 수 있습니다.

기존엔 properties를 이용하여 다루었지만, 최근에 yaml(yml) 파일을 통해 명시하고 있습니다.



먼저, java>resource> 밑에

application.properties 파일을 -> application.yml로 변경


application.yml 설정


# profiles 값 할당 전
server:
port: 18080
---
# profiles value 할당 (방법1)
# java -jar 파일명.jar --spring.profiles.active=profiles값 (택1)
# java -Dspring.profiles.active=profiles값 -jar 파일명.jar (택2)
spring:
profiles: local
server:
port: 8080
---
# profiles value 할당 (방법2)
# Edit Configurations > Run/Debug Configurations > Active Profiles 값 할당
spring:
profiles: dev
server:
port: 8081
---
spring:
profiles: real
server:
port: 80

--- 를 기준으로 구분


위의 예는 was의 포트번호입니다.

profiles 값을 설정하지 않으면 최상단의 port가 적용



profiles 설정 방법


- command-line에서 설정


# java -jar 파일명.jar --spring.profiles.active=profiles값 (택1)

# java -Dspring.profiles.active=profiles값 -jar 파일명.jar (택2)


dev 또는 real 값을 입력



- Run/Debug Configuration을 통해 입력



Edit Configurations을 클릭



VM options에 -Dspring.profiles.active=dev 입력



서버를 구동하면, port가 dev의 8081로 할당된 것을 볼 수 있음



기존 localhost:8080 -> localhost:8081로 설젇 된 것을 확인




이런식으로 profiles를 할당하면 db같은 설정도 쉽게 할 수 있음


블로그 이미지

사용자 yhmane

댓글을 달아 주세요

  • 물고기 개발자 2019.12.27 16:16 신고  댓글주소  수정/삭제  댓글쓰기

    정보 감사합니다 인텔리 버전이 다를수도있는데 run/debug Configurations 에 Active profiles 라는 항목이 생겨서 local/dev/real 을 입력해서 변경이 가능하네요 혹시나해서 VM options 와 Active profiles에 각각 다른 프로필을 넣어봤는데 Active profiles 에 선언한 포트가 우선 작동하네요 참고요~

스프링 4.3에 release된 annotation 중 mapping 관련하여 포스팅을 하려고 합니다.


먼저, @RequestMapping은 url을 통해 들어온 request를 method와 연결하여 주는 annotation이었습니다.


여기에, 속성으로 method를 지정하여 


method = RequestMethod.GET


method = RequestMethod.POST 형식으로 지정하여 주었습니다.




하지만, 이러한 불편함을 대체하여 주고, 코드의 가독성을 높여주는 annotation들이 spring 4.3 버전 이후로 출시가 되었습니다.


간단하게 코드로 알아보겠습니다.



@RequestMapping(value = "/" , method = RequestMethod.GET)
public String getMethod(HttpServletRequest request, HttpServletResponse response) {
return "";
}

@GetMapping("/test/get")
public String getMethod2(HttpServletRequest request, HttpServletResponse response) {
return "";
}

@PostMapping("/test/post")
public String postMethod(HttpServletRequest request, HttpServletResponse response) {
return "";
}

@PutMapping("/test/put")
public String putMethod(HttpServletRequest request, HttpServletResponse response) {
return "";
}

@DeleteMapping("/test/delete")
public String deleteMethod(HttpServletRequest request, HttpServletResponse response) {
return "";
}

@PatchMapping("/test/patch")
public String patchMethod(HttpServletRequest request, HttpServletResponse response) {
return "";
}

위의 방식으로 기존 @RequstMapping( value = "", method = RequestMethod.XXXX)를 간략히 쓸 수 있습니다.






/**
* Annotation for mapping HTTP {@code GET} requests onto specific handler
* methods.
*
* <p>Specifically, {@code @GetMapping} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @RequestMapping(method = RequestMethod.GET)}.
*
*
* @author Sam Brannen
* @since 4.3
* @see PostMapping
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {

/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";



@GetMapping의 소스를 조금 더 까보면

@RequestMapping(method = RequestMethod.GET)이 선언 되어 있는 것을 볼 수 있습니다.




[참조]


Http Rest Service 예제 

https://spring.io/guides/tutorials/bookmarks/


Spring @RequestMapping New Shortcut Annotations 예제

https://www.baeldung.com/spring-new-requestmapping-shortcuts


블로그 이미지

사용자 yhmane

댓글을 달아 주세요