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

댓글을 달아 주세요

들어가며

SpringBatch를 처음 접하게 되면 아마 많은 분들이 이 문제를 겪지 않을까 생각합니다.

이 문제는 '지정된 Job이 아닌 모든 Job 실행' 입니다. 저도 최근에 Spring Batch를 접하며 겪은 문제이기도 합니다.
job name을 파라미터로 전해주었지만, 모든 Job들이 실행됩니다. 하지만, 우리는 모든 Job들이 동시간대에 실행되는 것을 원하지 않습니다.

그렇다면 어떻게 해결할 수 있을까요? 밑에서 바로 알아보도록 하겠습니다.


Job 구성

  1. Simple job
  2. Other Job
  3. application.yml

Simple Job 코드

@Slf4j
@RequiredArgsConstructor
@Configuration
public class SimpleJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job simpleJob() {
        return jobBuilderFactory.get("simpleJob")
            .start(simpleStep())
            .build();
    }

    @Bean
    public Step simpleStep() {
        return stepBuilderFactory.get("simpleStep")
            .tasklet((contribution, chunkContext) -> {
                log.info(">>>>> simple job started >>>>>");
                return RepeatStatus.FINISHED;
            })
            .build();
    }
}

Other Job 코드

@Slf4j
@Configuration
@RequiredArgsConstructor
public class OtherJobConfiguration {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job otherJob() {
        return jobBuilderFactory.get("otherJob")
            .start(otherStep())
            .build();
    }

    @Bean
    public Step otherStep() {
        return stepBuilderFactory.get("otherStep")
            .tasklet((contribution, chunkContext) -> {
                log.info(">>>>> other job started >>>>>");
                return RepeatStatus.FINISHED;
            })
            .build();
    }
}

applicatoin.yml

spring:
  profiles:
    active: local

---
spring:
  datasource:
    hikari:
      jdbc-url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
      username: sa
      password:
      driver-class-name: org.h2.Driver
  config:
    activate:
      on-profile: local

먼저, simpleJob, otherJob을 각각 생성하여 주고 yml을 설정하여 줍니다. 다음으로, Application의 argument를 다음과 같이 구성하여 줍니다.

--job.name=simpleJob version=1

job의 이름은 simpleJob이고 파라미터로 version=1을 주었습니다.
우리가 예상하는 결과는 console창에 ">>>>> simple job started >>>>>”이 찍히는 것입니다.

하지만, 예상과는 다르게 두개의 job이 실행되었습니다.
argument로 job.name을 전해주었지만
프레임워크 level에서는 어떠한 job이 실행되어야 하느지 알 수 없기에 모든 job을 실행하였습니다.


문제 해결

spring.batch.job.names: ${job.name:EMPTY}

이 문제를 해결하기 위해 yml에 위와 같은 property를 추가 해주도록 하겠습니다.


SpringBatch의 내부 BatchProperties 클래스를 들여다 보면 setNames 함수를 볼 수 있습니다.
위와 같이 정의된 job.names 값들을 Job으로 등록하여 줍니다. property를 yml에 추가하여 주고, 다시 실행해보도록 하겠습니다.

이제 원하는 Job이 정상적으로 실행되었습니다.

주의

여기서 argument로 job.names을 전달하지 않을 경우, 지정한 default EMPTY가 할당되게 됩니다.
즉, 이 소스상에서는 EMPTY로 할당된 Job이 없기 때문에 어떠한 Job도 실행되지 않습니다.

마무리

우리는 프로그램을 작성하며 Job을 하나만 작성하지 않습니다.
Server의 메모리 문제나, 실행되는 시간들이 다르기 때문에 일반적으로 Job이 나뉘어서 실행되기를 바랍니다.
따라서, yml에 해당 property 값을 적어주고 jar 실행시 적절한 argument를 전달하여 주면 됩니다!!


참조

jojoldu님 티스토리

블로그 이미지

사용자 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

댓글을 달아 주세요

Meta-Data Schema


아래의 테이블은 Spring Batch에서 제공하는 Meta-Data 테이블입니다.
해당 테이블들은 Batch Job의 실행을 기록하기 위해 존재합니다.

JobInstance   : BATCH_JOB_INSTANCE
JobExecution  : BATCH_JOB_EXECUTION
JobParameters : BATCH_JOB_EXECUTION_PARAMS
StepExecution : BATCH_STEP_EXECUTION
ExecutionContext : BATCH_JOB_EXECUTION_CONTEXT, BATCH_STEP_EXECUTION_CONTEXT
  • Spring Batch 메타 데이터 테이블은 Java로 표현하는 Domain 객체와 거의 일치합니다.
  • Java 객체(왼쪽)는 테이블(오른쪽)과 매핑됩니다.

위에 언급한 Java 도메인들은 전 포스팅에 언급하였기 때문에 이번 포스팅에선 설명을 생략하도록 하겠습니다.
이번 포스팅에서는 h2, mysql 데이터베이스 사용시 spring batch 설정방법과 meta 테이블의 prefix 설정방법에 대하여 알아보도록 하겠습니다.


Batch Job 설정, H2 실행

build.gradle -> dependency 설정

dependencies {
    compile('org.springframework.boot:spring-boot-starter-batch')
    compile('org.springframework.boot:spring-boot-starter-jdbc')
    runtime('com.h2database:h2')
    runtime('mysql:mysql-connector-java')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.springframework.batch:spring-batch-test')
}

DemoApplication.java 설정

@EnableBatchProcessing
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}
  • @EnableBatchProcessing
    이 어노테이션을 선언하여 Spring Batch의 여러 기능들을 사용할 수 있게 합니다. 선언하지 않으면 Spring Batch 기능을 사용할 수 없기에 필수로 선언하여 줍니다.

SimpleJobConfiguration.java 설정

@Slf4j
@RequiredArgsConstructor
@Configuration
public class SimpleJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job simpleJob() {
        return jobBuilderFactory.get("simpleJob")
            .start(simpleStep())
            .build();
    }

    @Bean
    public Step simpleStep() {
        return stepBuilderFactory.get("simpleStep")
            .tasklet((contribution, chunkContext) -> {
                log.info(">>>>> job started >>>>>");
                return RepeatStatus.FINISHED;
            })
            .build();
    }
}
  • @Configuration안의 @Bean으로 Job과 Step을 등록하여 줍니다.
  • Step의 경우 tasklet과 ItemReader, ItemProcessor, ItemWriter로 나눌수 있는데 이번 포스팅에선 단순한 Step 실행을 위해 tasklet으로 실행하도록 하겠습니다.

application.yml 설정 (h2 기준)

spring:
  profiles:
    active: local

---
spring:
  datasource:
    hikari:
      jdbc-url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
      username: sa
      password:
      driver-class-name: org.h2.Driver
  config:
    activate:
      on-profile: local
---

profiles를 active로 설정하여 실행 해보도록 하겠습니다.

Job이 수행되는것을 확인하였습니다.


mysql 환경에서 실행하기

application.yml에 mysql 환경 추가해주기

---
spring:
  datasource:
    hikari:
      jdbc-url: jdbc:mysql://localhost:3308/spring_batch
      username: root
      password: root
      driver-class-name: com.mysql.jdbc.Driver
  config:
    activate:
      on-profile: mysql


Host, username, password를 각 db 환경에 맞추어 설정해주세요.
(저의 경우에는 docker를 이용하였습니다)

mysql에 batch ddl 생성하여 주기

IntelliJ 기준으로 ‘shift’ + ‘command’ + ‘o’를 이용하여 ‘schema-mysql.sql’의 내용들을 이용해 batch 테이블을 생성하여 줍니다.
h2와 mysql은 테이블 설정이 살짝 다르니, 꼭 mysql.sql 파일을 이용해 배치 테이블을 생성해주세요!!


profile을 설정한 후 실행하여 줍니다.


메타테이블에 방금 실행한 job의 정보가 기록 되었습니다. 이런식으로, spring batch는 트랜잭션을 프레임워크레벨에서 관리하여 주며 개발자가 비즈니스 로직 개발에 집중할 수 있게 도와줍니다.


메타 테이블 prefix 설정하기

일반적으로는 ‘batch_ ‘ 라는 prefix를 주어 메타테이블을 관리하지만, 여러가지 이유로 다른 prefixf를 주어 관리하기도 합니다. 예를 들면 많은 application이 하나의 batch 테이블을 사용하게 되면 메타 정보가 섞일 수 있기 때문에 다소 복잡도가 생길 수 있습니다.

BatchConfiguration.java

@Configuration
public class BatchConfiguration extends DefaultBatchConfigurer {

    private static final String TABLE_PREFIX = "CUSTOM_";

    private final DataSource dataSource;

    public BatchConfiguration(DataSource dataSource) {
        super(dataSource);
        this.dataSource = dataSource;
    }

    @Override
    protected JobRepository createJobRepository() throws Exception {
        JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
        factory.setTransactionManager(super.getTransactionManager());
        factory.setDataSource(dataSource);
        factory.setTablePrefix(TABLE_PREFIX);
        factory.setIsolationLevelForCreate("ISOLATION_REPEATABLE_READ");
        return factory.getObject();
    }

    @Override
    protected JobExplorer createJobExplorer() throws Exception {
        JobExplorerFactoryBean factory = new JobExplorerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setTablePrefix(TABLE_PREFIX);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
}

DefaultBatchConfigurer 을 상속한 @Configuration을 하나 만들어 줍니다. 여기서 table prefix를 설정하여 줍니다. 저는 기존 ‘batch_’ prefix를 ‘custom_’ prefix로 설정하였습니다

다음으로 이전 mysql ddl script문을 이용하여 prefix 테이블을 새로 만들어 줍니다.

CREATE TABLE CUSTOM_JOB_INSTANCE  (
    JOB_INSTANCE_ID BIGINT  NOT NULL PRIMARY KEY ,
    VERSION BIGINT ,
    JOB_NAME VARCHAR(100) NOT NULL,
    JOB_KEY VARCHAR(32) NOT NULL,
    constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
) ENGINE=InnoDB;

CREATE TABLE CUSTOM_JOB_EXECUTION  (
    JOB_EXECUTION_ID BIGINT  NOT NULL PRIMARY KEY ,
    VERSION BIGINT  ,
    JOB_INSTANCE_ID BIGINT NOT NULL,
    CREATE_TIME DATETIME(6) NOT NULL,
    START_TIME DATETIME(6) DEFAULT NULL ,
    END_TIME DATETIME(6) DEFAULT NULL ,
    STATUS VARCHAR(10) ,
    EXIT_CODE VARCHAR(2500) ,
    EXIT_MESSAGE VARCHAR(2500) ,
    LAST_UPDATED DATETIME(6),
    JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL,
    constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)
    references CUSTOM_JOB_INSTANCE(JOB_INSTANCE_ID)
) ENGINE=InnoDB;
... 중략

prefix를 설정한 테이블에 batch job 실행이 기록되었습니다.

이번 포스팅에선 job 기록을 위한 meta 테이블 생성을 알아보았습니다. Spring batch는 여러 커스터마이징 기능을 제공하는데 메타 테이블도 간단한 설정을 위와 같이 할 수 있습니다!!

다음 포스팅에서는 Step에 대해 자세히 알아보도록 하겠습니다.


참조

spring.io
jojoldu님 블로그

블로그 이미지

사용자 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

댓글을 달아 주세요




1회독 완료하였습니다.


JPA에 관심 있다면, 또는 프로젝트에 도입을 고려하고 있다면 무조건 구매하세요!!


책이 750페이지가 조금  안 되는데 .. 


하루 30페이지씩 읽는다면 한달이면 완독합니다




Java ORM에 조금이라도 관심이 있으시다면 구매 추천합니다!


- 에이콘 / 김영한님 지음


'Books' 카테고리의 다른 글

이것이 Fedora 리눅스다  (0) 2019.11.23
자바 ORM 표준 JPA 프로그래밍  (0) 2019.07.23
처음 배우는 스프링 부트2  (0) 2019.07.07
Effective JAVA 3/E (이펙티브 자바 3판)  (0) 2019.02.07
블로그 이미지

사용자 yhmane

댓글을 달아 주세요




스프링부트를 공부할 겸 '처음배우는 스프링부트2' 책을 구매하였고, 

이제 완독하였습니다.


이 책의 특징을 나열하자면?

  • 비교적 최신에 나온 spring boot 책이다
  • 컨텐츠가 좋다 (oauth2, security, rest, batch)

총평은

 스프링부트에 대해 관심이 있고 2주 정도의 시간을 갖고 공부를 하기에 유용한 책으로 판단됩니다. 
자세한 설명들은 생략 되어 있는 부분들이 있기에, 1년 이상 스프링 공부를 하셨고,
스프링부트에 입문하려고 할 때 좋은 책으로 생각됩니다~



- 한빛미디어/김영재님 지음

'Books' 카테고리의 다른 글

이것이 Fedora 리눅스다  (0) 2019.11.23
자바 ORM 표준 JPA 프로그래밍  (0) 2019.07.23
처음 배우는 스프링 부트2  (0) 2019.07.07
Effective JAVA 3/E (이펙티브 자바 3판)  (0) 2019.02.07
블로그 이미지

사용자 yhmane

댓글을 달아 주세요