@ComponentScan 애노테이션은 spring 3.1부터 도입됐으며 설정된 시작 지점부터 컴포넌트 클래스를 scanning하여 빈으로 등록해주는 역할을 한다.
컴포넌트 클래스는 다음 애노테이션이 붙은 클래스를 의미한다.
@Component
@Repository
@Service
@Controller
@Configuration
@ComponentScan의 가장 중요한 두 가지 속성은component를 scan할 시작 지점을 설정하는 속성과scan한 component 중 빈으로 등록하지 않을 클래스를 제외하는 필터 속성이다.
component-scan은 기본적으로 @Component 어노테이션을 빈 등록 대상으로 포함한다. 그렇다면 @Controller 나 @Service는 어떻게 인식하는 걸까? 그 이유는 @Controller나 @Service가 @Component를 포함하고 있기 때문이다.
component-scan 사용방법
component-scan 을 사용하는 방법은 xml 파일에 설정하는 방법, 과자바파일안에서 설정하는 방법이 있다.
위와 같이 설정하면, base pacakage 하위의 @Controller, @Service @Repository, @Component 클래스가 모두 빈으로 등록되므로, 특정한 객체만 빈으로 등록하여 사용하고 싶다면 include-filter나exclude-filter를 통해 설정할 수 있다.
use-default="false"는 기본 어노테이션 @Controller, @Component등을 스캔하지 않는다는 것이다. 기본 어노테이션을 스캔하지 않는다고 설정하고,include-filter를 통해서 위와 같이 특정 어노테이션만 스캔할 수 있다.
2. 자바 파일안에서 설정
@Configuration
@ComponentScan(basePackages = "com.rcod.lifelog")
public class ApplicationConfig {
}
@Configuration은 이 클래스가 xml을 대체하는 설정 파일임을 알려준다. 해당 클래스를 설정 파일로 설정하고@ComponentScan을 통하여 basePackages를 설정해준다.
위와 같이component-scan을 사용하는 두 가지 방법이 있다. 만약 component-scan을 사용하지 않으면, 빈으로 설정할 클래스들을 우리가 직접 xml 파일에 일일이 등록해 주어야 한다.
<bean id="mssqlDAO" class="com.test.spr.MssqlDAO"></bean>
<!-- MemberList 객체에 대한 정보 전달 및 의존성 주입 -->
<bean id="member" class="com.test.spr.MemberList">
<!-- 속성의 이름을 지정하여 주입 -->
<property name="dao">
<ref bean="mssqlDAO"/>
</property>
</bean>
MssqlDAO와MemberList를 빈으로 등록하고, MemberList에 Mssql을 주입한 것이다. 위와 같이 코드가 매우 길어지고, 일일이 추가하기에 복잡해진다.
component-scan 동작 과정
ConfigurationClassParser 가 Configuration 클래스를 파싱한다. @Configuration 어노테이션 클래스를 파싱하는 것이다. ⬇ ComponentScan 설정을 파싱한다. base-package 에 설정한 패키지를 기준으로 ComponentScanAnnotationParser가 스캔하기 위한 설정을 파싱한다. ⬇ base-package 설정을 바탕으로 모든 클래스를 로딩한다. ⬇ ClassLoader가 로딩한 클래스들을 BeanDefinition으로 정의한다. 생성할 빈의 대한 정의를 하는 것이다. ⬇ 생성할 빈에 대한 정의를 토대로 빈을 생성한다.
Spring boot에서의 @ConponentScan
이전 Xml Config 방식에서 ComponentScan을 사용하는 방법은 다음과 같았다.
applicationContext를 구성할때 이렇게 명시적으로 내가 읽어들여야하는 component들이 있는 package를 넣어줬다.
하지만 Springboot에서는 Xml Config보다는 Java Config를 사용하고 @기반의 설정을 많이 한다. 아니 이 Component Scan을 하지도 않는데 알아서 잘 된다. 어떻게 된 일일까? 바로 Springboot의 핵심! @SpringBootApplication 에 답이 있다. Springboot Main Class에 있는 @SpringBootApplication를 ctrl을 누르고 눌러서 들어가보자.
들어가보면 이런식으로 구성이 되어있다. 복잡해 보이지만 쉽게 설명을 하자면 내가 아무런 ComponentScan 관련 설정을 하지 않았다면 바로 이@SpringBootApplication 가 정의된 곳이 base package가 되는 것이다. 그래서 처음 프로젝트 구조를 만들때 이 Springboot Main Class의 package가 매우 중요하다.
그리고 아래에 나와있는 @AliasFor 부분에 나온basePackages와 basePackagesClasses도 중요하다.Springboot Main Class의 위치에 구애받지 않고 내가마음대로 ComponentScan을 할 곳을 정의할때 사용된다.
스프링 배치의 모든 잡은 실패하거나 중지될 때 다시 실행할 수 있었다. 스프링 배치는 기본적으로 이렇게 동작하므로 우리는 다시 실행하면 안되는 잡이 있을시 재시작을 방지해야한다.
preventRestart()
preventRestart() 메서드를 호출하여 잡이 실패하거나 어떤 이유로 중지된 경우에도 다시 실행할 수 없다.
/**
* 잡 실행
* @return
*/
@Bean
public Job transactionJob() {
return this.jobBuilderFactory.get("transactionJob")
.preventRestart() /* 잡은 기본적으로 재시작이 가능하도록 구성되어있다. 잡의 재시작 방지 */
.start(...)
.next(...)
.build();
}
잡의 재시작 횟수 제한
startLimit(n) : 재시작 횟수를 n번으로 제한한다.
@Bean
public Step importTransactionFileStep() {
return this.stepBuilderFactory.get("importTransactionFileStep")
.startLimit(2) /* 잡의 재시작 횟수 제한 */
.<Transaction, Transaction>chunk(100)
.reader(...)
.writer(...)
.listener(...) /* 스텝 빌드하기 전 실행할 리스너 등록 */
.build();
}
완료된 스텝 재실행하기
allowStartIfComplete(true)
스텝이 잘 완료됐더라도, 다시 실행할 수 있어야 할때 사용한다.주의할 점은, 잡의 ExitStatus 가 COMPLETE 라면 모든 스텝에allowStartIfComplete(true) 를 적용하더라도 이와 관계없이 잡 인스턴스는 다시 실행할 수 없다.
잡이 재실행될때 무조건 실행되어야할 스텝이 존재할 경우 설정한다.
@Bean
public Step importTransactionFileStep() {
return this.stepBuilderFactory.get("importTransactionFileStep")
.<Transaction, Transaction>chunk(100)
.reader(...)
.writer(...)
.allowStartIfComplete(true) /* 잡이 재시작될시, 스텝이 다시 실행될 수 있도록 재시작 허용 */
.listener(...) /* 스텝 빌드하기 전 실행할 리스너 등록 */
.build();
}
일반적으로 Spring Batch는 단일 쓰레드에서 실행됩니다. 즉, 모든 것이 순차적으로 실행되는 것을 의미하는데요. Spring Batch에서는 이를 병렬로 실행할 수 있는 방법을 여러가지 지원합니다. 이번 시간에는 그 중 하나인 멀티스레드로 Step을 실행하는 방법에 대해서 알아보겠습니다.
Spring Batch의 멀티쓰레드 Step은 Spring의TaskExecutor를 이용하여각 쓰레드가 Chunk 단위로 실행되게하는 방식입니다.
여기서 어떤TaskExecutor를 선택하냐에 따라 모든 Chunk 단위별로 쓰레드가 계속 새로 생성될 수도 있으며 (SimpleAsyncTaskExecutor) 혹은 쓰레드풀 내에서 지정된 갯수의 쓰레드만을 재사용하면서 실행 될 수도 있습니다. (ThreadPoolTaskExecutor)
Spring Batch에서 멀티쓰레드 환경을 구성하기 위해서 가장 먼저 해야할 일은 사용하고자 하는Reader와 Writer가 멀티쓰레드를 지원하는지확인하는 것 입니다.
JpaPagingItemReader의 Javadoc
각 Reader와 Writer의 Javadoc에 항상 저thread-safe문구가 있는지 확인해보셔야 합니다. 만약 없는 경우엔 thread-safe가 지원되는 Reader 와 Writer를 선택해주셔야하며, 꼭 그 Reader를 써야한다면SynchronizedItemStreamReader등을 이용해thread-safe로 변환해서 사용해볼 수 있습니다.
그리고 또 하나 주의할 것은 멀티 쓰레드로 각 Chunk들이 개별로 진행되다보니 Spring Batch의 큰 장점중 하나인 실패 지점에서 재시작하는 것은 불가능 합니다. 이유는 간단합니다. 단일 쓰레드로 순차적으로 실행할때는 10번째 Chunk가 실패한다면 9번째까지의 Chunk가 성공했음이 보장되지만, 멀티쓰레드의 경우 1~10개의 Chunk가 동시에 실행되다보니 10번째 Chunk가 실패했다고 해서 1~9개까지의 Chunk가 다 성공된 상태임이 보장되지 않습니다.
그래서 일반적으로는 ItemReader의 saveState 옵션을 false 로 설정하고 사용합니다.
이건 예제 코드에서 설정을 보여드리겠습니다.
자 그럼 실제로 하나씩 코드를 작성하면서 실습해보겠습니다.
2. PagingItemReader 예제
가장 먼저 알아볼 것은 PagingItemReader를 사용할때 입니다. 이때는 걱정할 게 없습니다. PagingItemReader는Thread Safe하기 때문입니다.
멀티 쓰레드로 실행할 배치가 필요하시다면 웬만하면 PagingItemReader로 사용하길 추천드립니다.
예제 코드는 JpaPagingItemReader로 작성하였습니다.
@Slf4j
@RequiredArgsConstructor
@Configuration
public class MultiThreadPagingConfiguration {
public static final String JOB_NAME = "multiThreadPagingBatch";
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final EntityManagerFactory entityManagerFactory;
private int chunkSize;
@Value("${chunkSize:1000}")
public void setChunkSize(int chunkSize) {
this.chunkSize = chunkSize;
}
private int poolSize;
@Value("${poolSize:10}") // (1)
public void setPoolSize(int poolSize) {
this.poolSize = poolSize;
}
@Bean(name = JOB_NAME+"taskPool")
public TaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // (2)
executor.setCorePoolSize(poolSize);
executor.setMaxPoolSize(poolSize);
executor.setThreadNamePrefix("multi-thread-");
executor.setWaitForTasksToCompleteOnShutdown(Boolean.TRUE);
executor.initialize();
return executor;
}
@Bean(name = JOB_NAME)
public Job job() {
return jobBuilderFactory.get(JOB_NAME)
.start(step())
.preventRestart()
.build();
}
@Bean(name = JOB_NAME +"_step")
@JobScope
public Step step() {
return stepBuilderFactory.get(JOB_NAME +"_step")
.<Product, ProductBackup>chunk(chunkSize)
.reader(reader(null))
.processor(processor())
.writer(writer())
.taskExecutor(executor()) // (2)
.throttleLimit(poolSize) // (3)
.build();
}
@Bean(name = JOB_NAME +"_reader")
@StepScope
public JpaPagingItemReader<Product> reader(@Value("#{jobParameters[createDate]}") String createDate) {
Map<String, Object> params = new HashMap<>();
params.put("createDate", LocalDate.parse(createDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")));
return new JpaPagingItemReaderBuilder<Product>()
.name(JOB_NAME +"_reader")
.entityManagerFactory(entityManagerFactory)
.pageSize(chunkSize)
.queryString("SELECT p FROM Product p WHERE p.createDate =:createDate")
.parameterValues(params)
.saveState(false) // (4)
.build();
}
private ItemProcessor<Product, ProductBackup> processor() {
return ProductBackup::new;
}
@Bean(name = JOB_NAME +"_writer")
@StepScope
public JpaItemWriter<ProductBackup> writer() {
return new JpaItemWriterBuilder<ProductBackup>()
.entityManagerFactory(entityManagerFactory)
.build();
}
}
(1)@Value("${poolSize:10}")
생성할 쓰레드 풀의 쓰레드 수를 환경변수로 받아서 사용합니다.
${poolSize:10}에서 10은 앞에 선언된 변수poolSize가 없을 경우 10을 사용한다는 기본값으로 보시면 됩니다.
배치 실행시 PoolSize를 조정하는 이유는실행 환경에 맞게 유동적으로 쓰레드풀을 관리하기 위함입니다.
개발 환경에서는 1개의 쓰레드로, 운영에선 10개의 쓰레드로 실행할 수도 있습니다.
혹은 같은 시간대에 수행되는 다른 배치들로 인해서 갑자기 쓰레드 개수를 줄여야 할 수도 있습니다.
언제든 유동적으로 배치 실행시점에 몇개의 쓰레드를 생성할지 결정할 수 있으니 웬만하면 외부에 받아서 사용하는 방식을 선호합니다.
Field가 아닌 Setter로 받는 이유는 Spring Context가 없이 테스트 코드를 작성할때 PoolSize, ChunkSize등을 입력할 방법이 없기 때문입니다.
(2)ThreadPoolTaskExecutor
쓰레드 풀을 이용한 쓰레드 관리 방식입니다.
옵션
corePoolSize: Pool의 기본 사이즈
maxPoolSize: Pool의 최대 사이즈
이외에도SimpleAsyncTaskExecutor가 있는데, 이를 사용할 경우매 요청시마다 쓰레드를 생성하게 됩니다.
이때 계속 생성하다가 concurrency limit 을 초과할 경우 이후 요청을 막게되는 현상까지 있어, 운영 환경에선 잘 사용하진 않습니다.
먼저 비교해볼 것은테이블의 ID 생성 전략이 없는 경우입니다. (즉, Auto Increment가 없는 상태) 이는Id 채번을 애플리케이션에서 하는 경우입니다. 테스트 할 Entity 는 다음과 같습니다.
@Getter
@NoArgsConstructor
@Entity
public class Person2 {
@Id
private Long id;
private String name;
public Person2(Long id, String name) {
this.id = id;
this.name = name;
}
}
보시다시피@Id외에@GeneratedValue(strategy)를 선언하지 않은 상태입니다. 해당 Entity에 대한 Merge 테스트 코드는 다음과 같습니다.
@Test
public void non_auto_increment_test_merge() throws Exception {
// given
JpaItemWriter<Person2> writer = new JpaItemWriterBuilder<Person2>()
.entityManagerFactory(this.entityManagerFactory)
.build();
writer.afterPropertiesSet();
List<Person2> items = new ArrayList<>();
for (long i = 0; i < TEST_COUNT; i++) {
items.add(new Person2(i, "foo" + i));
}
// when
writer.write(items);
}
Reader로 인한 조회 성능 차이나 그 밖에 Spring Batch의 여러 요소들로 인해 방해되는 것을 막기 위해 순수 Writer만 가지고 테스트를 진행합니다.
위 테스트 코드를 수행해보면?
Select쿼리와 Insert쿼리가 함께 수행되는 것을 볼 수 있는데, 이는 Hibernate의 Merge 작동 방식때문인데요.
기존에 해당 Id로 저장된 Entity가 있을 경우 Update를, 없을 경우엔 Insert를 실행하기 위하여저장하는 Entity 개수만큼 Select 쿼리가 발생합니다.
반대로 Persist에선 어떻게 작동할까요?
아래와 같이 테스트 코드를 작성하여 실행해봅니다.
@Test
public void non_auto_increment_test_persist() throws Exception {
// given
JpaItemWriter<Person2> writer = new JpaItemWriterBuilder<Person2>()
.usePersist(true) // (1)
.entityManagerFactory(this.entityManagerFactory)
.build();
writer.afterPropertiesSet();
List<Person2> items = new ArrayList<>();
for (long i = 0; i < TEST_COUNT; i++) {
items.add(new Person2(i, "foo" + i));
}
// when
writer.write(items);
}
(1).usePersist(true)
글 상단에서 언급한것처럼 Spring Batch 4.2에서 도입된persist모드를 활성화하는 옵션입니다.
테스트 로그를 보면?
Merge와 달리Insert쿼리만 발생한 것을 확인할 수 있습니다.
자 그럼 이 둘의 실제 성능 차이는 얼마나 발생할까요?
Non Auto Increment 성능 비교
1만건의 Entity를 AWS RDS Aurora (r5.large) 에 밀어넣어보면서 비교해보겠습니다.
1. Merge
2. Persist
테스트 결과약 2배(merge: 2m 16s, persist: 1m 9s) 의 성능 차이가 발생하는 것을 확인할 수 있습니다.
Id 생성 전략이 별도로 없을 경우 Persist가 좋다는 것을 확인할 수 있습니다.
1-2. Auto Increment
자 그럼 반대로 Auto Increment가 선언 된 경우엔 어떻게 될까요?
테스트에 사용될 Entity는 다음과 같습니다.
@Getter
@NoArgsConstructor
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Person(String name) {
this.name = name;
}
}
가장 먼저 Merge에 대한 테스트 코드입니다.
@Test
public void auto_increment_test_merge() throws Exception {
// given
JpaItemWriter<Person> writer = new JpaItemWriterBuilder<Person>()
.entityManagerFactory(this.entityManagerFactory)
.build();
writer.afterPropertiesSet();
List<Person> items = new ArrayList<>();
for (long i = 0; i < TEST_COUNT; i++) {
items.add(new Person( "foo" + i));
}
// when
writer.write(items);
// then
assertThat(personRepository.count()).isEqualTo(TEST_COUNT);
}
이번 테스트부터는Id 생성을 Auto Increment에 맡기기 때문에직접 생성하지 않도록 하였습니다.
위 코드를 실행해보면?
추가 Select 쿼리 없이Insert만 수행되는 것을 확인할 수 있습니다. 지정된 Id가 없으니 명확하게 새로운 Entity 임을 알 수 있기 때문에 별도의 Select 쿼리가 발생하지 않았습니다.
그럼 Persist는 어떻게 될까요?
@Test
public void auto_increment_test_persist() throws Exception {
// given
JpaItemWriter<Person> writer = new JpaItemWriterBuilder<Person>()
.usePersist(true)
.entityManagerFactory(this.entityManagerFactory)
.build();
writer.afterPropertiesSet();
List<Person> items = new ArrayList<>();
for (long i = 0; i < TEST_COUNT; i++) {
items.add(new Person( "foo" + i));
}
// when
writer.write(items);
// then
assertThat(personRepository.count()).isEqualTo(TEST_COUNT);
}
Persist 테스트 코드를 수행해보면?
마찬가지로Insert쿼리만 수행되는 것을 확인할 수 있습니다.
Persist의 경우항상 새로운 객체를 저장할 때만사용해야 합니다. Auto Increment에서 ID가 있는 Entity를 저장할 경우 에러가 발생합니다.
둘 사이에 쿼리가 차이가 없으니 실제로 성능비교를 한번 해보겠습니다.
Auto Increment 성능 비교
실제 발생하는 쿼리가 동일하니 성능 역시 비슷하게 나옵니다.
1. Merge
2. Persist
둘의 수행속도가 비슷하니 Auto Increment인 경우에 써도 되지 않을까? 싶으실텐데요.
실제 Merge는Entity 복사를 매번 수행합니다. PersistenceContext에 존재하는 것을 반환하거나 Entity의 새 인스턴스를 만듭니다. 어쨌든 제공된 Entity에서 상태를 복사하고 관리되는복사본을 반환합니다. (전달한 인스턴스는 관리되지 않습니다.)
그래서 성능이 비슷하다 하더라도신규 Entity를 생성할때는 Persist를 사용하는 것이 좋습니다.
2. Jdbc Batch Insert
위 테스트들을 거치면서 한가지 의문이 있으셨을 것입니다.
JpaItemWriter는 왜 Batch Insert (혹은 Bulk Insert) 로 처리하지 않는 것이지?
일반적으로 Batch Insert라 하면 아래와 같은 쿼리를 이야기 합니다.
INSERT INTO person (name) VALUES
('name1'),
('name2'),
('name3');
이렇게 할 경우 MySQL 매커니즘으로 인해서 고성능으로 대량의 데이터를 처리할 수 있는데요.
실제 성능 비교를 아래에서 진행합니다.
JPA (정확히는 Hibernate) 에서는Auto Increment 일 경우엔 이 방식을 지원하지 않습니다.
1) 부모 Entity를 insert 하고 생성된 Id 반환 2) 자식 Entity에선 1) 에서 생성된 부모 Id를 FK 값으로 채워서 insert
위 과정를 진행하는 쿼리를 모아서 실행하는게 Hibernate의 방식인데, 이때 Batch Insert과 같은 대량 등록의 경우엔 이 방식을 사용할 수가 없습니다. (부모 Entity를 한번에 대량 등록하게 되면, 어느 자식 Entity가 어느 부모 Entity에 매핑되어야하는지 알 수 없겠죠?)
그럼 ID 생성 전략을 Auto Increment가 아닌 Table (Sequence)를 선택하면 되지 않을까 생각하게 되는데요. 아래 글에서 자세하게 설명하고 있지만,성능상 이슈와Dead Lock에 대한 이슈로 Auto Increment를 강력하게 추천합니다.
@Test
public void non_auto_increment_test_jdbc() throws Exception {
//given
JdbcBatchItemWriter<Person2> writer = new JdbcBatchItemWriterBuilder<Person2>()
.dataSource(dataSource)
.sql("insert into person(id, name) values (:id, :name)")
.beanMapped()
.build();
writer.afterPropertiesSet();
List<Person2> items = new ArrayList<>();
for (long i = 0; i < TEST_COUNT; i++) {
items.add(new Person2(i, "foo" + i));
}
// when
writer.write(items);
}
1만건을 요청하는 위 코드를 직접 MySQL에 요청을 해보면?
0.586초라는 JpaItemWriter에 비해 압도적인 성능을 보여줍니다.
3-2. Auto Increment 성능
그럼 Auto Increment일 경우엔 어떻게 될까요?
@Test
public void auto_increment_test_jdbc() throws Exception {
//given
JdbcBatchItemWriter<Person> writer = new JdbcBatchItemWriterBuilder<Person>()
.dataSource(dataSource)
.sql("insert into person(name) values (:name)")
.beanMapped()
.build();
writer.afterPropertiesSet();
List<Person> items = new ArrayList<>();
for (long i = 0; i < TEST_COUNT; i++) {
items.add(new Person( "foo" + i));
}
// when
writer.write(items);
}
동일하게 1만건을 요청할 경우에도 마찬가지로0.561초라는 결과를 보여줍니다.
순수하게 단일 테이블의 등록면에 있어서는 Jdbc Batch Insert의 성능이 비교가 안될 정도로 좋다는 것을 알 수 있습니다.
다만 무조건 많은 양의 row를 한번에 요청하는게 빠른 방법은 아닙니다. 한번에 몇개의 insert value를 만들지 MySQL의max_allowed_packet,Buffer Size,bulk_insert_buffer_size등 여러 옵션들에 따라 상이하니 적절한 성능 테스트를 통해 값을 찾아야 합니다.
3. 최종 비교
최종적으로 Spring Batch ItemWriter들의 성능을 비교하면 다음과 같습니다.
ItemWriter ModeNon Auto Increment (10,000 row)Auto Increment (10,000 row)
ItemWriter Mode
Non Auto Increment
Auto Increament
Jpa.Merge
2m 16s
1m 1s
Jpa.Persist
1m 9s
1m 2s
Jdbc Batch Insert
0.586s
0.586s
순수하게단일 테이블에 대량으로 등록할 경우 Jdbc의 Batch Insert 방식이 압도적인 성능을 보여줍니다. 다만, 무조건 Jdbc Batch Insert 방식을 사용하기엔 아래와 같은 단점들이 있습니다.
OneToMany, ManyToMany와 같이 복잡한 Entity 관계가 insert가 필요할 경우 직접 구현해야할 부분이 너무나 많이 존재
컴파일체크, 타입힌트, 자동완성등 유지보수가 어려운 개발 환경
그래서 다음과 같이혼합 방식을 선택하기도 합니다.
이를 테면 OneToMany의 관계가 등록이 필요할 경우
부모 Entity는 JpaItemWriter를 이용하여 ChunkSize별로 저장하여 PK값과 Entity를 확보
PK가 확보된 부모 Entity를 통해 자식 Entity들을 생성 (부모 ID값을 갖고 생성)
나는 주로 그 동안 자바를 메인으로 사용하기 때문에 자바랑 비교 해 보자면 무지 간단하다. java는 설정이 꽤나 복잡하다. JDK, 메이븐, 스프링 설정, web.xml 등 DB 설정등 무지 많다. 근데 php 는 xampp 하나만 깔면 새로 프로그램을 깔거나 할 것이 없다. editor 정도만 깔아 주면 된다.
괜찮은 MVC 프레임웍이 있다.
사실 설정이 간단한 건 ASP도 간단하다. 하지만 old asp는 만들거나 기존 소스가 없으면 db connection 부터 화면단까지 모두 개발자의 몫이다. 프레임웍이 없다는 건 생산성에도 문제가 있지만 프레임웍 없이 만든 코드는 가독성역시 떨어지고 유지보수도 어렵다. php에는 CI(code igniter)라고 하는 괜찮은 MVC 프레임웍이 있다. Java 의 스프링 만큼 좋은 것은 아니겠지만 web 개발하는데는 충분하다. MVC라는 구조를 아는 사람이라면 쉽게 접근할 수 있다. 이 건 동일한 스크립트 언어인 ASP와 비교해볼 때 우위를 가지는 장점이다.
스크립트 언어다.
스크립트 언어라는 것의 의미는 변경사항이 생겼을 때 서버 재시작이 필요 없는 것을 의미 한다. java 의 경우에는 query 등 java 파일을 변경하는 경우에는 기본적으로 서버를 리스타트가 필요하다. 실제 개발할 때 수정 내용을 바로 반영해서 화면을 보는 것과 서버를 다시 리스타트해서 보는 것 사이에는 작게는 몇배에서 크게는 몇 십배 아니 몇 백배의 시간 차이가 난다. 하지만 스크립트 언어라 가지는 단점도 존재한다. 간단하게는 에러가 나봐야 안다. 하지만 그만큼의 생산성이 단점을 커버할만하다고 생각한다.
멀티라인 문자열 변수를 사용할 수 있다.(?)
이건 Java 대비 가지는 장점인데 java는 멀티라인 스트링 변수를 사용할 수 없다. 때문에 복잡한 query나 설정은 xml 에 저장해야 한다. 이거 무지 번거롭다. 파일 위치를 정해야 하고 web 이라는 특성상 절대경로를 사용하기에는 부담스럽다. xml을 읽어서 parsing 해야 한다. 아님 mybatis 등 프레임웍을 설정해야 한다. 또 java 개발의 경우에는 java 파일 외에 properties 나 xml 파일을 다룰 줄 알아야 한다. 이렇게 하던 저렇게 하던 비용(시간)이 든다. php는 단순히 변수에 저장하면 되므로 간단하다. 또 문자열 연결도 무지 간단하다.
PHP 라는 Web 개발언어는 호불호가 많이 갈리는 언어이다. 그 나름의 주장이 의미가 있다.내가 장점으로 보는 것은 생산성 측면에서 좋다는 것이다. 그리고 스크립트 언어라는 단점 때문에 대용량이나 안정성이 중요하게 요구 되는 시스템에서는 적절하지 않을 수 있다. 다만 간단한 게시판 성의 내용이고 UI가 중요하다면 서버는 최대한 간단하게 php로 구축하고 UI를 유행하는 javascript 프레임웍(jquery, angular.js, react)으로 꾸며서 작업할 것이다. 또 내 경험으로 예전에 java 로 서버를 개발하고 html 에서 ajax를 만들어 보기가 쉽지 않았다. 지금은 RestController 등이 있어서 쉽게 json 형태로 변환이 되지만 얼마전까지는 쉽지 않았던 기억이 난다. 결론은 내가 100 이내의 사용자가 사용하는 내부 시스템을 만들어야 하는 상황이라면 PHP를 최우선으로 고려해 볼 듯 하다.
Spring Batch에서의 Chunk란 데이터 덩어리로 작업 할 때각 커밋 사이에 처리되는 row 수를 얘기합니다. 즉, Chunk 지향 처리란 한 번에 하나씩 데이터를 읽어 Chunk라는 덩어리를 만든 뒤, Chunk 단위로 트랜잭션을 다루는 것을 의미합니다. 여기서 트랜잭션이라는게 중요한데요. Chunk 단위로 트랜잭션을 수행하기 때문에실패할 경우엔 해당 Chunk 만큼만 롤백이 되고, 이전에 커밋된 트랜잭션 범위까지는 반영이 된다는 것입니다. Chunk 지향 처리가 결국 Chunk 단위로 데이터를 처리한다는 의미이기 때문에 그림으로 표현하면 아래와 같습니다.
Chunk 지향 프로세싱은 1000개의 데이터에 대해 배치 로직을 실행한다고 가정하면, Chunk 단위로 나누지 않았을 경우에는 한개만 실패해도 성공한 999개의 데이터가 롤백된다. Chunk 단위를 10으로 한다면, 작업 중에 다른 Chunk는 영향을 받지 않는다.
Reader에서 데이터 하나를 읽어 온다.(item 단위)
읽어온 데이터를 Processor에서 가공한다.(item 단위)\
가공된 데이터들을 별도의 공간에 모은 뒤 Chunk 단위만큼 쌓이게 되면 Writer에 전달하고 Writer는 일괄 저장한다. (Chunk = items)
여기선 Reader, Processor에서는 1건씩 다뤄지고, Writer에서는 Chunk 단위로 처리된다는 것을 기억하면 된다.
Chunk-oriented processing의 장점
위에서 알아본청크지향 프로세싱을 사용하지 않는다 하더라도 개발자가 충분히 비슷한 로직으로 구현을 할 수도 있습니다. 하지만청크지향 프로세싱은 단순히 청크단위의트랜잭션만 제공해주는것은 아닙니다.
Spring batch 청크지향 프로세싱의 가장 큰 장점이라고 하면, 내결함성 (Falut tolernat)를 위한 다양한 기능들을 제공하고 있다는 것 입니다.
멀티 스레드 환경에서 chunk 지향 불가
각 Reader와 Writer의 Javadoc에 항상 저thread-safe문구가 있는지 확인해보셔야 합니다. 만약 없는 경우엔 thread-safe가 지원되는 Reader 와 Writer를 선택해주셔야하며, 꼭 그 Reader를 써야한다면SynchronizedItemStreamReader등을 이용해thread-safe로 변환해서 사용해볼 수 있습니다.
그리고 또 하나 주의할 것은 멀티 쓰레드로 각 Chunk들이 개별로 진행되다보니 Spring Batch의 큰 장점중 하나인 실패 지점에서 재시작하는 것은 불가능 합니다. 이유는 간단합니다. 단일 쓰레드로 순차적으로 실행할때는 10번째 Chunk가 실패한다면9번째까지의 Chunk가 성공했음이 보장되지만, 멀티쓰레드의 경우 1~10개의 Chunk가 동시에 실행되다보니 10번째 Chunk가 실패했다고 해서1~9개까지의Chunk가 다 성공된 상태임이 보장되지 않습니다. 그래서 일반적으로는 ItemReader의saveState옵션을false로 설정하고 사용합니다.
@Service
public class BugService {
private BugRepository bugRepository;
@Autowired
public BugService(BugRepository bugRepository){
this.bugRepository = bugRepository;
}
}
Field 변수를 이용한 방법
장점
간단하다.
단점
의존 관계가 눈에 잘 보이지 않아 추상적이고, 이로 인해 의존성 관계가 과도하게 복잡해질 수 있다.
SRP : 단일책임원칙에 반하는 안티패턴
단위 테스트시 의존성 주입이 용이하지 않음
@Service
public class BugService {
@Autowired
private BugRepository bugRepository;
}
setter를 이용한 방법
장점
의존성이 선택적으로 필요한 경우에 사용(set을 불러올때 사용하니깐!?)
생성자에 모든 의존성을 기술하면 과도하게 복잡해질 수 있는 것으로 선택적으로 나눠 주입 할 수 있게 부담을 덜어줌
생성자 주입과 setter 주입 방법을 적절하게 분배하여 사용을 권장
@Service
public class BugService {
private BugRepository bugRepository;
@Autowired
public void setBugRepository(BugRepository bugRepository) {
this.bugRepository = bugRepository;
}
}
DI(Dependency Injection) 세 가지 방법
느슨한 결합
- 객체를 주입 받는다는 것은 외부에서 생성된 객체를 인터페이스를 통해서 넘겨받는 것이다. 이렇게 하면 결합도를 낮출수 있고, 런타임시에 의존관계가 결정되기 때문에 유연한 구조를 가진다.
- SOULD 원칙에서 O에 해당하는 Open Closed Principle을 지키기 위해서 디자인 패턴 중 전략 패턴을 사용하게 되는데, 생성자 주입을 사용하게 되면 전략 패턴을 사용하게 된다.
Fielid Injection 필드 주입
@Component
public class SampleController {
@Autowired
private SampleService sampleService;
}
Field Injection을 사용하면 안되는 이유
단일 책임(SRP)의 원칙 위반
의존성을 주입하기가 쉽다. @Autowired 선언 아래 개수 제한 없이 무한정 추가할 수 있으니 말이다.
여기서Constructor Injection을 사용하면 다른 Injection 타입에 비해 위기감을 느끼게 해준다.
Constructor의 parameter가 많아짐과 동시에 하나의 Class가 많은 책임을 떠안는다는 걸 알게된다.
이때 이러한 징조들이 Refactoring을 해야한다는 신호가 될 수 있다.
Setter Injection 세터 주입
@Component
public class SampleController {
private SampleService sampleService;
@Autowired
public void setSampleService(SampleService sampleService) {
this.sampleService = sampleService;
}
}
Construtor Injection 생성자 주입 (권장)
아래 처럼 Constructor에 @Autowired Annotation을 붙여 의존성을 주입받을 수 있다.
@Component
public class SampleService {
private SampleDAO sampleDAO;
@Autowired
public SampleService(SampleDAO sampleDAO) {
this.sampleDAO = sampleDAO;
}
}
@Component
public class SampleController {
private final SampleService sampleService = new SampleService(new SampleDAO());
...
}
Spring Framework Reference에서 권장하는 방법은 생성자를 통한 주입이다.
생성자를 사용하는 방법이 좋은 이유는필수적으로 사용해야하는 의존성 없이는 Instance를 만들지 못하도록 강제할 수 있기 때문이다.
Spring 4.3버전부터는 Class를 완벽하게DI Framework로부터 분리할 수 있다.
단일 생성자에 한해 @Autowired를 붙이지 않아도 된다. Spring 4.3부터는 클래스의 생성자가 하나이고 그 생성자로 주입받을 객체가 Bean으로 등록되어 있다면 @Autowired를 생략할 수 있다.
또한 앞서 살펴본Field Injection의 단점들을 장점으로 가져갈 수 있다.
null을 주입하지 않는 한 NullPointerException 은 발생하지 않는다.
의존관계 주입을 하지 않은 경우에는 Controller 객체를 생성할 수 없다. 즉, 의존관계에 대한 내용을 외부로 노출시킴으로써 컴파일 타임에 오류를 잡아낼 수 있다.
final 을 사용할 수 있다.
final로 선언된 레퍼런스타입 변수는 반드시 선언과 함께 초기화가 되어야 하므로 setter 주입시에는 의존관계 주입을 받을 필드에 final 을 선언할 수 없다.
final의 장점은 객체가 불변하도록 할 수 있는 점으로,누군가가 Controller 내부에서 Service 객체를 바꿔치기 할 수 없다는 점이다.
순환 의존성을 알 수 있다.
앞서 살펴 본Field Injection에서는 컴파일 단계에서 순환 의존성을 검출할 방법이 없지만,Construtor Injection에서는 컴파일 단계에서 순환 의존성을 잡아 낼 수 있다.
의존성을 주입하기가 번거로워 위기감을 느낄 수 있다.
Construtor Injection의 경우 생성자의 인자가 많아지면 코드가 길어지며개발자로 하여금 위기감을 느끼게 해준다.
이를 바탕으로 SRP 원칙을 생각하게 되고, Refactoring을 하게 된다.
이러한 장점들 때문에스프링 4.x Documents에서는Constructor Injection을 권장한다.
굳이Setter Injection을 사용하려면, 합리적인 default를 부여할 수 있고 선택적인(optional) 의존성을 사용할 때만 사용해야한다고 말한다. 그렇지 않으면 not-null 체크를 의존성을 사용하는 모든 코드에 구현해야한다.
결국더 좋은 디자인 패턴과 코드 품질을 위해서는Constructor Injection을 사용해야 한다.