TDD 란?

TDD는 Test-Driven-Development로써 직역하면 테스트 주도 개발입니다. 네…. 말그대로 테스트를 중심으로 개발한다는 개발방법론인데요. 간단하게 정의하면 기능이되는 코드 이전에 테스트코드를 작성해서 검증한 뒤 기능코드를 작성하는 방법입니다. 저같은 초급 개발자에겐 많이 생소한 느낌이지만 이미 많은 기업에서도 TDD를 적용하고 있습니다.

SW개발에 있어서 개발시간을 가장 크게 증가시키는 요인중에 하나가 바로 버그입니다. 이 버그는 Test가 정확하게 이루어지지 않아서 발생한다고 볼 수 있죠. TDD를 사용하면 선 테스트 후 개발이기 때문에 테스트를 거치지 않은 코드가 없습니다. 버그의 발생률을 상당히 줄여주는 이유기기도 하죠. 실제로 “TDD의 적용 여부에 따라서 개발시간이 30%가량 단축된다.” 라는 글도 있을 정도로 TDD는 SW개발에서 상당히 중요한 역할을 하고 있습니다.

TDD의 궁극적인 목표는 “Clean code that works” 인데요. 깔끔하고 잘 동작하는 코드입니다. TDD의 과정을 요약하면 다음과 같은데요

 

  1. 테스트를 작성한다.
  2. 작성한 테스트를 통과할 수 있도록 가장 빠른 방법으로 코드를 작성한다. 이 과정에 중복된 코드를 만들어도 상관 없다.
  3. 테스트를 수행한다.
  4. 테스트를 통과하면 작성한 코드에서 중복을 제거한다. 아니면 2번으로 돌아간다.
  5. 테스트를 수행한다.
  6. 테스트를 통과하면 완성. 다음 테스트를 1번부터 시작한다. 실패하면 4로 돌아가서 디버깅한다.

 

자세한 내용은 이곳 , 저곳을 확인하시기 바랍니다.

 

JUnit 이란?

 

JUnit은 java 진영에서 가장 강력하고 유명하게 사용하는 테스트 프레임워크입니다. TDD에서 아주 중요한 역할을 해줍니다. 주요 메서드와 어노테이션을 알아보는 시간을 가져볼께요.

 

JUnit Assert Class

Assert class의 메서드들은 전부 static으로 선언되어 있기 때문에 Assert.assertEquals(“”, “”); 이런식으로 사용해도 되지만 편의성을 높이기 위해 보통은 class파일 최상단(package 바로 밑)에 import static org.junit.Assert.*; 이런식으로 정적선언 해준 뒤 메서드명만 사용하는게 일반적입니다.

Assert class 메서드의 true,false 값은 Test 메서드의 성공여부를 나타냅니다.

 

assertEquals()

void assertEquals(boolean expected, boolean actual)

두개의 값이 같을경우 true 아니면 false 예상값을 첫번째 파라미터에 넣어줍니다.

 

assertFalse()

void assertFalse(boolean condition)

넘어온 값이 false면 true , true면 false

 

assertTrue()

void assertTrue(boolean condition)

넘어온 값이 true면 true , false면 false

 

assertNotNull()

void assertNotNull(Object object)

넘어온 Object가 null이면 false, null이 아니면 true

 

assertNull()

void assertNotNull(Object object)

넘어온 Object가 null이면 true, null이 아니면 false

 

fail()

void fail()

어떠한 경우라도 fail()을 만나면 Test결과 실패

 

JUnit Annotaion

 

@Test

@Test를 붙임으로써 이 메서드가 테스트 케이스로 실행될 수 있음을 나타내줍니다. 반드시 public void methodName 형태여야 합니다.

 

@Before

@Before는 @Test 가 붙은 메서드가 실행되기 이전에 동작합니다. 보통 객체를 초기화시키는 작업에서 많이 사용하고 메서드 명은 setup을 사용합니다.

 

@After

@After는 @Before와 비슷하게 메서드가 실행되고 난 후에 동작합니다. 주로 변수설정을 다시하거나 임시파일을 삭제하는곳에서 사용합니다.

 

@Ignores

@Ignores는 말 그대로 테스트를 사용하지 않고 싶을때 사용합니다.

 

@Test(timeout=500)

@Test에 timeout 필드는 이 테스트에 시간제한을 명시해주는 필드입니다. 이 필드의 값은 ms 기준이고 테스트가 0.5초가 지나면 테스트 결과는 실패로 간주됩니다.

 

@Test(expected=IllegalArgumentException.class)

@Test에 expected 필드는 이 테스트가 던지는 예외를 지정할 수 있습니다. 만약 IllegalArgumentException.class가 테스트 도중 이 메서드로 던져지면 테스트 결과는 성공, 아니면 실패로 간주됩니다.

 

@RunWith(SpringRunner.class)

@Runwith는 JUnit 프레임워크의 확장입니다. 이를 이용하여 자신에게 필요한 테스트 러너를 직접 만들어서 자신만의 고유한 기능을 추가해 테스트를 수행할 수 있습니다. JUnit 5 이상 버전에서는 더이상 @Runwith가 아니라 @ExtendWith를 사용합니다.

 

@SpringBootTest

@SpringBootTest는 Spring legacy의 @ContextConfiguration 어노테이션을 SpringBoot에서 사용할 수 있도록 대체해주는 역할을 합니다. 이 어노테이션은 ApplicationContext를 만들고 테스트할 수 있도록 해줍니다. Spring boot 기준으로 반드시@RunWith(SpringRunner.class) 도 명시해주어야하며 이 또한 JUnit5 이상일 경우 @ExtendWith로 대체할 수 있습니다.

 

MockObject 란?

 

일단 Mock이란 직역하면 모조품이란 뜻을 가지고 있습니다. 객체지향 프로그래밍에서 Mock Object는 application을 테스트할 때 주로 사용되는 모의 객체입니다. spring-boot-starter-test 모듈에서도 역시 java mocking framework인 Mockito를 내장하고 있습니다.

 

A mock object can be useful in place of a real object that:

  1. Runs slowly or inefficiently in practical situations
  2. Occurs rarely and is difficult to produce artificially
  3. Produces non-deterministic results
  4. Does not yet exist in a practical sense
  5. Is intended mainly or exclusively for conducting tests.

 

이 Mock을 사용하는 이유는 실제 A객체를 테스트해야하는데 A객체가 B객체와 의존되어있는 상황에서 B객체와 의존성을 단절시키기 위해 Mock으로 대체하여 테스트를 진행할 수 있습니다. Mock은 주로 Test하기 어려운 DB가 묶인 상황에도 많이 사용됩니다.

 

이 Mock의 사용은 예제에서 한번 다뤄보겠습니다. 예제는 Mockito를 사용합니다.

 

테스트 더블에 대한 이야기는 링크 에서 아주 정확하게 얘기해주고 있으니 참고 바랍니다.

 

Example (Spring boot 2.1.3) JUnit + Mockito

이번예제는 간단하게 TDD + JUnit + Mockito를 사용하여 Test해보는 시간입니다.

Spring boot 기준으로 예제를 생성하면 다음과 같이 pom.xml에 spring-boot-starter-test 모듈이 추가되는데요

 

- pom.xml

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

 

spring-boot-starter-test 모듈은 아래와 같은 라이브러리를 포함하고 있습니다.

 

  • JUnit: The de-facto standard for unit testing Java applications.
  • Spring Test & Spring Boot Test: Utilities and integration test support for Spring Boot applications.
  • AssertJ: A fluent assertion library.
  • Hamcrest: A library of matcher objects (also known as constraints or predicates).
  • Mockito: A Java mocking framework.
  • JSONassert: An assertion library for JSON.
  • JsonPath: XPath for JSON.

 

따라서 기존 legacy 프로젝트처럼 junit 모듈을 추가하지 않아도 돼요!

 

예제 시나리오는 회원가입 비지니스 로직에서 id,pw 를 가진 User.class를 db에 넣기 이전에 pw값을 암호화하여 db에 넣어주는 예제입니다. 물론 실 db는 안들어가요.

 

먼저 간단하게 풀어서 써보면

  1. 앞단에서 사용자 id, pw를 입력하고 회원가입 요청을 한다
  2. 서버는 id, pw가 유효한지 검증한다 (ex : id는 5자 이상, pw는 특수문자 포함 8자 이상 등..)
  3. 검증이 끝나면 pw를 암호화 알고리즘을 통해서 암호화 된 값으로 변환한다.
  4. 검증과 암호화가 끝난 id,pw를 db에 넣는다

정도가 될꺼 같습니다.

 

(3과 4는 join()에서 한번에 이루어지기 때문에 한번에 처리합니다.)

 

- User.class

package com.sup2is.demo;

public class User {
	
	private String id;
	private String pw;
    
    
    public User(String id, String pw) {
		this.id = id;
		this.pw = pw;
	}

    
    //getter...
    //setter...
    
	
}

 

먼저 필요한 VO객체 입니다. 그냥 id와 pw 필드만 있는 간단한 User객체입니다.

 

다음은 JoinService interface입니다.

 

- JoinService.interface

package com.sup2is.demo;

public interface JoinService {
	
	public User join(User user);
	
	public boolean verify(User user);

}

 

메서드는 join, verify를 갖고 있는 interface 골격이 되겠습니다.

 

다음은 JoinService를 구현하는 몸체가 되는 JoinServiceImpl.class 입니다

- JoinServiceImple.class

package com.sup2is.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class JoinServiceImpl implements JoinService {

    //@Autowired 런타임 에러를 위해 cryptoService는 required false로 줌
	@Autowired(required=false)
	private CryptoService cryptoService;
	
	@Override
	public User join(User user) {
		//user pw 암호화
		user.setPw(cryptoService.encrypt(user.getPw()));
		
		//join ... db에 넣는 로직 .. 
		return user;
	}

	@Override
	public boolean verify(User user) {
		
		//verify ...
		
		return false;
	}

}

 

verify 부분에서는 id,pw의 검증을 통해서 boolean값을 리턴하게 만들었습니다. 아직은 method 몸체랑 단순 return값 밖에 없는 상태예요.

 

join부분은 verify가 true일 경우에 동작하는데 주입받은 cryptoService를 이용해 cryptoService.encrypt(user.getPw()); 를 이용하여 암호화하는 부분입니다. 실 db에 넣지 않기 때문에 값을 확인하기 위하여 User객체를 반환합니다.

 

마지막으로

- CryptoService.interface

package com.sup2is.demo;

public interface CryptoService {
	
	public String encrypt(String pw);

}

그냥 매우 간단하게 암호화에 필요한 encrypt() 메서드가 들어있습니다.

그리고 아직 CryptoService에 대한 스펙은 명시가 되어있지 않습니다. 아직 어떤 알고리즘을 사용하기로 얘기하지 않았기 때문이죠.

 

시나리오대로 Test Case를 작성해보겠습니다

 

1.앞단에서 사용자 id, pw를 입력하고 회원가입 요청을 한다

 

이제 Test 클래스를 작성해보겠습니다. 이에 앞서 Test클래스를 만들어놓고 여러개의 테스트를 작성해야하는데 메서드 네임이 도저히 생각나지 않을때는 링크 를 참고하시기 바랍니다!

 

MethodName_StateUnderTest_ExpectedBehavior

There are arguments against this strategy that if method names change as part of code refactoring than test name like this should also change or it becomes difficult to comprehend at a later stage. Following are some of the example:

  • isAdult_AgeLessThan18_False
  • withdrawMoney_InvalidAccount_ExceptionThrown
  • admitStudent_MissingMandatoryFields_FailToAdmit

 

저는 1번 방식으로 사용해보겠습니다. 후달리는 영어지만 되도록 영어를 사용하도록 노력해봅시다 …

 

@Autowired를 사용하기 때문에 Spring context가 필요하니 @Runwith와 @SpringBootTest를 명시해줍니다.

 

- JoinServiceTest.class

package com.sup2is.demo;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class JoinServiceTest {
	
	@Autowired
	private JoinService joinService;

	@Test
	public void testVerify_provideUser_verifyIsTrue() {
		//given
		User user = new User("sup2is", "12345678");
		
		//when
		boolean isValid = joinService.verify(user);
			
		//then
		assertEquals(isValid, true);
	}

}

 

모든 Test Case는 given,when,then으로 역할을 명확하게 지정해서 하시면 좀 더 읽기 좋은 Test Case를 구성하실 수 있습니다.

 

given에는 User객체를 생성해주고 실제 user 객체를 사용할 then에서는 verify() 메서드를 호출해줬습니다. when에서의 결과 확인을하기 위해 assertEquals() 메서드를 호출해주었습니다.

 

assertEquals(isValid,true); 를 사용하여 값비교를 실행했습니다. 결과는 당연히 실패겠죠? 아직 저희는 verify()의 몸체를 작성하지 않고 return false로만 해뒀으니까요. 따라서 verify() 메서드를 수정해줍니다.

 

2.서버는 id, pw가 유효한지 검증한다 (ex : id는 5자 이상, pw는 특수문자 포함 8자 이상 등..)

 

- JoinServiceImpl.class

	@Override
	public boolean verify(User user) {
		
		// id는 5자 이상 pw는 8자 이상
		if(user.getId().length() >= 5 && user.getPw().length() >= 8) {
			return true;
		}
		
		return false;
	}

다시 테스트를 실행해봅니다. 초록불뜨는걸 확인했습니다. verify에서 특수문자도 검사해야하지만 id pw의 길이만 검사하기로 했습니다.

 

이제 join()를 테스트해보도록 하겠습니다.

 

3.검증이 끝나면 pw를 암호화 알고리즘을 통해서 해시값으로 변환한다.검증과 암호화가 끝난 id,pw를 db에 넣는다

 

4.검증과 암호화가 끝난 id,pw를 db에 넣는다

 

- JoinServiceTest.class

	@Test
	public void testVerify_provideUser_verifyIsTrue() {
		//given
		User user = new User("sup2is", "12345678");
		
		//when
		boolean isValid = joinService.verify(user);
			
		//then
		assertEquals(isValid, true);
	}
	
	@Test
	public void testJoin_provideUser_joinSuccess() {
		
		//given
		User user = new User("sup2is", "12345678");
		
		//when
		User encryptUser = joinService.join(user);
		
		//then
		assertEquals("encrypted String", encryptUser.getPw());
		
	}

 

테스트를 작성해보니 메서드 중복이 존재합니다. user를 생성해주는 부분을 JUnit에서 제공하는 @Before를 사용해서 중복제거를 해보도록하겠습니다.

 

- JoinServiceTest.class

//...	
 	
	private User user;
	
	@Before
	public void setup() {
		user = new User("sup2is", "12345678");
	}


//...


	@Test
	public void testVerify_provideUser_verifyIsTrue() {
		//given
		
		//when
		boolean isValid = joinService.verify(user);
			
		//then
		assertEquals(isValid, true);
	}
	
	@Test
	public void testJoin_provideUser_joinSuccess() {
		
		//given
		
		//when
		User encryptUser = joinService.join(user);
		
		//then
		assertEquals("encrypted String", encryptUser.getPw());
		
	}

 

이제 testJoin_provideUser_joinSuccess() 를 실행해보겠습니다. 네 당연히 에러가 나겠죠? 저희는 아직 joinService에서 사용하는 cryptoService를 생성하지 않았으니까요. NullPointerException를 뿜뿜합니다.

 

근데 cryptoService의 상세 스펙이 아직도 정해지지않았다면 어떻게해야할까요? 아직 어떤 암호화 알고리즘을 사용할 지 결정되지 않았다면 테스트를 중지해야할까요?

 

이런상황에서 사용하는게 바로 mock입니다. mock객체를 사용해서 모조품을 만들고 join메서드를 완성시키도록 하겠습니다.

 

mockito에서 제공하는 어노테이션을 사용해서 joinservice 내부에 @Autowired로 받는 cryptoService를 mock객체로 주입해보겠습니다.

 

먼저 사용될 어노테이션에대해 간단하게 설명드립니다.

 

@Mock

@Mock은 Mock 객체를 생성해줍니다. mockito에서 제공하는 mock() 메서드와 동일한 역할을 합니다.

 

@InjectMocks

@InjectMocks 는 Mock객체가 필요한 객체에 Mock객체를 연결시켜주는 역할을 합니다.

 

*이런 … JoinService 타입이 interface이기 때문에 @InjectMocks으로 인스턴스화 할 수 없군요 .. ㅠㅠ 어쩔 수 없이 JoinServiceTest.class의 joinService 필드를 JoinServiceImpl 타입으로 변경하였습니다. 이 예제는 만들면서 진행한것이기 때문에 위에는 별도의 처리를 하지 않겠습니다.

 

joinService의 ctyptoService를 Autowired하기 위해 @Autowired로 주입받던 joinService를 @InjectMocks을 사용하여 Mockito에게 인스턴스화를 떠넘깁니다. 그와 동시에 @Mock을 사용하여 cryptoService를 mock객체로 생성해줍니다.

 

이제 마저 테스트를 실행해보겠습니다.

 

- JoinServiceTest.class

	@InjectMocks // <-- @Autowired에서 @InjectMocks로 변경
	JoinServiceImpl joinService; //<-- 구현체로 변경
	
	@Mock // <-- mock 객체로 생성
	CryptoService cryptoService; 
	

// ...


	@Test
	public void testJoin_provideUser_joinSuccess() {
		
		//given
		when(cryptoService.encrypt(user.getPw())).thenReturn("encrypted String");
		
		//when & then
		User encryptUser = joinService.join(user);
		assertEquals("encrypted String", encryptUser.getPw());
		
	}

 

Test는 성공으로 떨어집니다. mockito에서 제공하는 when().thenReturn메서드를 통해 cryptoService.encrypt() 메서드의 몸체를 구현하지 않고 단순히 “encrypted String” 을 반환하게 만들었습니다. 

이렇게 JoinService의 join() 메서드가 cryptoService의 encrypt() 메서드에게 의존하고 있음에도 불구하고 Mock을 사용하여 다른 CryptoService의 구현체 없이 테스트를 통과할 수 있었습니다. 앞서 말씀드린 의존관계를 어느정도 단절시키는 아주 좋은 예가 되었으면 좋겠습니다.

 

사실 저도 이번기회에 TDD 와 Mock을 제대로 접한거라 틀린 정보가 있을 수도 있고 예제 구현 방식에 있어서 미흡한 부분이 있을 수 있으니 참고바랍니다.

 

포스팅은 여기까지 하겠습니다. 모든예제는 제 github에서 확인하실 수 있습니다.

예제 : https://github.com/sup2is/spring-example/tree/master/springframework-3/src

 

spring 주제는 일단 여기까지로 하고 다음시간부터는 관심있는 주제부터 하나하나씩 포스팅 할 예정입니다.

 

퍼가실때는 링크와 출처를 반드시 명시해주세요. 감사합니다.

 

출처: http://agiledata.org/essays/tdd.html

출처: http://www.nextree.co.kr/p11104/

출처 : https://repo.yona.io/doortts/blog/issue/1

출처 : https://web.archive.org/web/20070628064054/http://xper.org/wiki/xp/TestDrivenDevelopment

출처 : https://junit.org/junit5/

출처 : https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-testing

출처 : https://doortts.tistory.com/169

출처 : https://dzone.com/articles/7-popular-unit-test-naming

출처 : https://web.archive.org/web/20061012050617/http://xper.org/wiki/xp/TDD_bc_f6_b7_c3_b9_fd

출처 : https://web.archive.org/web/20070628064054/http://xper.org/wiki/xp/TestDrivenDevelopment

출처 : https://www.guru99.com/junit-annotations-api.html

출처 : https://codedragon.tistory.com/5507

출처 : https://searchsoftwarequality.techtarget.com/definition/mock-object

출처 : http://hyeonjae-blog.logdown.com/posts/679308

출처 : https://stackoverflow.com/questions/16467685/difference-between-mock-and-injectmocks

 

 

원본 : https://sup2is.github.io/spring-framework-3/

 

Spring Framework #3 Test편

 

sup2is.github.io

 

AOP 란 ?

간단하게 용어설명을 하자면 Aspect-Oriented Programming인데요. 직역하면 관점지향 프로그래밍입니다. 네.. 해석한 한글이 더 어렵구요… 설명하기 쉽게 비슷한 개념으로 Java에서 사용중인 OOP가 있습니다. Object-Oriented Programming 이죠. 이건 많이 들어보셔서 아시겠지만 바로 객체지향 프로그래밍입니다. Java에서는 Object 즉 클래스 위주의 프로그래밍으로 하나의 프로그램을 구현하잖아요? AOP도 같은 개념입니다. 관점을 지향하는 프로그래밍 방식이예요.

AOP는 주로 공통관심사에 사용되는데 가장 대표적인게 logging, transaction, 인증 등의 처리가 되겠습니다. AOP에서는 cross-cutting이라는 개념이 있는데 이게 바로 logging, transaction, 인증이 아주 좋은 예가 될 수 있을꺼 같네요.

AOP를 사용하는 이유에 대해서 조금 더 설명하면 다음과같은 Foo.class는 여러개의 메서드를 갖고 있어요.

class Foo {


    public void a(){ 
        //something.. 
    }
    public void a2(){ 
        //something.. 
    }
    public void a3(){ 
        //something.. 
    }
    public void b(){ 
        //something.. 
    }
    public void c(){ 
        //something.. 
    }
    public void c2(){ 
        //something.. 
    }
    public void d(){ 
        //something.. 
    }

}

만약 a로 시작하는 메서드의 시작과 끝부분에 log를 남겨 메서드의 실행시간을 체크하고 싶은 경우가 있을 수 있죠. 그렇다면 이때 a로 시작하는 메서드 시작과 끝부분에 로직을 추가해 메서드의 실행시간을 체크할 수 있습니다. 하지만 별로 좋은생각은 아니죠 만약 Foo.class의 a메서드 뿐만 아니라 수많은 클래스의 a메서드, 또는 프로그램에서 호출하는 모든 메서드에 실행시간을 체크하고 싶다면? 저 방법으로는 어느정도 한계가 있죠. 그래서 사용하는게 AOP입니다.

AOP 용어

  • Advice

Advice는 제공할 서비스를 나타내는데요. aspect가 무엇을 언제 할지 정의하는 역할을 합니다.

Spring에서는 총 5개의 관점이 있습니다.

  1. Before : 메소드 실행 전 Advice 실행
  2. After : 메소드 실행 후 Advice 실행
  3. After-returning : 메서드가 성공 후(예외 없이) Advice 실행
  4. After-throwing : 메서드가 예외발생 후 Advice 실행
  5. Around : 메소드 실행 전과 후 Advice 실행 (Before + After)
  • Joinpoint

JoinPoint는 AOP를 적용할 수 있는 지점을 나타냅니다. Spring AOP에서 join point는 항상 메소드 실행을 나타냅니다.

  • Pointcut

Pointcut은 표현식이나 패턴들을 활용하는 AOP의 EL이라고 생각하시면 되고 하나 또는 여러개의 joinpoint 집합입니다.

  1. execution(public * *(..))

    - 모든 public 메서드에 실행

  2. execution(* set*(..))

    - set으로 시작하는 모든 메서드에 실행

  3. execution(* com.sup2is.service.AccountService.*(..))

    - com.sup2is.service.AccountService 안에 모든 메서드에 실행

  4. execution(* com.sup2is.service.*.*(..))

    - com.sup2is.service 패키지 안에 모든 메서드에 실행

  5. execution(* com.sup2is.service..*.*(..))

    - com.sup2is.service의 서브패키지를 포함한 패키지 안에 모든 메서드에 실행

  6. within(com.sup2is.service.*)

    - com.sup2is.service 패키지 안에 모든 joinpoint에 실행

  7. within(com.sup2is.service..*)

    - com.sup2is.service의 서브패키지를 포함한 패키지 안에 모든 joinpoint에 실행

이 외에도 this, target,arg,@target,@within 등등이 있습니다.

  • Introduction

Introduction을 통해 기존 클래스에 새 메소드나 특성을 추가 할 수 있습니다.

  • Aspect

여러 객체에 공통 관심사 cross-cutting 개념을 갖고 있습니다. 위에서 설명한 logging,transaction,인증이 아주 좋은 예입니다.

  • Weaving

Weaving은 advice를 다른 application 또는 Object와 관점을 연결하여 개체를 만드는 프로세스입니다. Weaving은 Compile tile, Run time, Load time에 실행될 수 있습니다. Spring AOP 에서는 Runtime에 동작합니다.

  • Target Object

Target Object는 말 그대로 Advice가 적용된 하나 또는 여러개의 관점들 입니다. Spring AOP는 Runtime Proxy를 사용하여 구현되기 때문에 Spring proxy 객체로 알려져 있습니다.

용어 설명은 이정도로 마치고.. 예제를 통해서 직접 사용해보는게 좋을것 같아요

Example (Spring boot 2.1.3)

먼저 간단한 시나리오는 덧셈,뺄셈,나눗셈,곱셈의 동작을 하는 Calculator 객체가 있는데 이 메서드들에게 적절한 pointcut EL을 사용하여 log를 찍어보는 예제입니다. 매우 간단해요.

- MyCalculator.class

package com.sup2is.demo;

import org.springframework.stereotype.Component;

@Component
public class MyCalculator {

    public int add(int a, int b) {
        System.out.println("### add method 실행");
        return a + b;
    }

    public int sub(int a, int b) {
        System.out.println("### sub method 실행");
        return a - b;
    }

    public int division(int a, int b) {
        System.out.println("### division method 실행");
        return a / b;
    }

    public int multiply(int a, int b) {
        System.out.println("### multiply method 실행");
        return a * b;
    }
}

네 .. 정말 간단하네요 ..

이제 cross-cutting을 적용해볼께요.

@Before

- CalculationAspect.class

package com.sup2is.demo;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect //Aspect 명시
@Component //반드시 Component 등의 Spring Bean으로 등록해야 Aspect가 제대로 적용됨
public class CalculationAspect {

    // com.sup2is.demo.MyCalculator class 내부의 모든 메서드에 실행
    @Before("execution(* com.sup2is.demo.MyCalculator.*(..))") 
    public void beforeLog(JoinPoint joinPoint) {
        System.out.println("### " + joinPoint.getSignature().getName() +
                           " : before execute");
    }

}

이 CalculationAspect.class 에서 주목해야 하는부분은 바로 beforeLog() 메서드의 파라미터인 org.aspectj.lang.Joinpoint interface인데요. @Before, @After, @AfterReturning, @AfterThrowing 에 선택적으로 파라미터를 명시하면 자동으로 메서드에 파라미터가 넘어오게됩니다. 이 joinpoint는 getSignature() 메서드 처럼 AOP가 호출된 메서드의 정보 등이 넘어오니 자세한 내용은링크를 직접확인해보시는게 좋을 것 같아요.

- SpringDemoApplicationTests.class

package com.sup2is.demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringDemoApplicationTests {

    @Autowired
    private MyCalculator myCalculator;

    @Test
    public void addTest() {
        System.out.println(myCalculator.add(5, 5));
    }
    @Test
    public void subTest() {
        System.out.println(myCalculator.sub(5, 5));
    }
    @Test
    public void divisionTest() {
        System.out.println(myCalculator.division(5, 5));
    }
    @Test
    public void multiplyTest() {
        System.out.println(myCalculator.multiply(5, 5));
    }

}

저는 @Before 어노테이션을 사용해서 MyCalculator.class 내부에 동작하는 모든 메서드에

### 메서드 이름 : before execute 라는 로그를 찍게 구현해봤어요. 실행해보겠습니다.

- console

...
### sub : before execute
### sub method 실행
0
### division : before execute
### division method 실행
1
### add : before execute
### add method 실행
10
### multiply : beforeexecutee
### multiply method 실행
25

생각보다 간단하죠? 저는 분명 MyCalculator.class 내부에 log를 넣어놓은 적이 없는데 AOP가 알아서 log를 남겨주고 있습니다.

@After

마찬가지로 @After는 메서드 실행 이후에 동작합니다.

- CalculationAspect.class

    // com.sup2is.demo.MyCalculator class 내부의 add 메서드에 실행
    @After("execution(* com.sup2is.demo.MyCalculator.add(..))") 
    public void afterLog(JoinPoint joinPoint) {
        System.out.println("### " + joinPoint.getSignature().getName() +
                           " : after execute");
    }

@After는 add라는 메서드에만 동작하게 해놨는데요. 실행해보면

- console

### sub : before execute
### sub method 실행
0
### division : before execute
### division method 실행
1
### add : before execute
### add method 실행
### add : after execute
10
### multiply : before execute
### multiply method 실행
25

보시는것처럼 add 메서드에만 @After 가 동작한걸 확인하실 수 있어요.

@AfterReturing

@AfterReturing은 메서드가 예외 없이 성공적으로 끝났을때 호출이 되는데요.

이번에는 add 메서드와 division 메서드에 동작시키도록 해 보겠습니다. 메서드명에 공통적으로시작하는 접두어가 있다면 com.sup2is.demo.MyCalculator.find*(..) 으로 표현식을 작성 할 수 있지만 그렇지 않다면 && 와 를 이용해 표현식을 연결 할 수 있습니다.

 

 

- CalculationAspect.class

    // com.sup2is.demo.MyCalculator class 내부의 add,division 메서드에 실행
    @AfterReturning("execution(* com.sup2is.demo.MyCalculator.add(..) || execution(* com.sup2is.demo.MyCalculator.division(..)" )
    public void afterReturning(JoinPoint joinPoint) {
        System.out.println("### " + joinPoint.getSignature().getName() +" : after returning execute");
    }

myCalculator.division() 메서드 파라미터에 5와 0을 넘겨서 의도적으로java.lang.ArithmeticException 발생시킵니다.

- SpringDemoApplicationTests.class

    @Test
    public void addTest() {
        System.out.println(myCalculator.add(5, 5));
    }

 ... 

    @Test
    public void divisionTest() {
        System.out.println(myCalculator.division(5, 0));
    }

- console

### sub : before execute
### sub method 실행
0
### division : before execute
### division method 실행  <-- java.lang.ArithmeticException 발생

### add : before execute
### add method 실행
### add : after execute
### add : after returning execute
10
### multiply : before execute
### multiply method 실행
25

성공적으로 값을 반환한 add메서드와는 달리 division은 @AfterRiturning이 적용되지 않은걸 확인하실 수 있습니다.

추가적으로 @AfterReturning은 반환한 return값을 가져올 수 있는 returning 필드가 @AfterReturning 어노테이션 필드에 내장되어 있는데요. 말그대로 Aspect가 적용된 메서드의 return값을 메서드 내부에서 사용할 수 있습니다. 아주 유용하게 쓰일 수 있죠.

- CalculationAspect.class

    // com.sup2is.demo.MyCalculator class 내부의 add,division 메서드에 실행
    @AfterReturning(pointcut = "execution(* com.sup2is.demo.MyCalculator.add(..)) ||"
             + " execution(* com.sup2is.demo.MyCalculator.division(..))" , 
                    returning="value")
    public void afterReturning(JoinPoint joinPoint, Integer value) {
        System.out.println("### " + joinPoint.getSignature().getName() +
                           " : after returning execute");
        System.out.println("### value : " + value );
    }

- console

### add : before execute
### add method 실행
### add : after execute
### add : after returning execute
### value : 10
10

@AfterThrowing

@AfterThrowing은 메서드실행중 예외가 발생했을때만 동작합니다.

    // com.sup2is.demo.MyCalculator class 내부의 division,multiply 메서드에 실행
    @AfterThrowing(pointcut = "execution(* com.sup2is.demo.MyCalculator.division(..)) ||"
            + " execution(* com.sup2is.demo.MyCalculator.multiply(..))", throwing="ex")
    public void afterThrowing(JoinPoint joinPoint, ArithmeticException ex) {
        System.out.println("### " + joinPoint.getSignature().getName() +
                           " : afterThrowing execute");
        System.out.println("### " + ex.getMessage() +
                           " : exception occurred");
    }

@AfterThrowing 어노테이션은 throwing이라는 필드를 갖고 있는데요. 예외가 발생했을때 Aspect 내부에 Exception 객체를 전달해주는 역할을 합니다. 파라미터에 Exception.class를 입력하면 Exception전부를 잡아내지만 타입을 강하게 ArithmeticException.class로 준다면 ArithmeticException이 발생된 메서드만 @AfterThrowing이 발생합니다.

- MyCalculator.class

    public int multiply(int a, int b) {
        System.out.println("### multiply method 실행");

        if(a == 0) {
            throw new IllegalArgumentException();
        }

        return a * b;
    }

간단하게 multiply 메서드를 조금 수정해줬는데요. 첫번째 파라미터가 0이면 IllegalArgumentException(); 를 반환하게 수정했습니다.

- SpringDemoApplicationTests.class

    @Test
    public void divisionTest() {
        System.out.println(myCalculator.division(5, 0));
    }
    @Test
    public void multiplyTest() {
        System.out.println(myCalculator.multiply(0, 5));
    }

- console

### sub : before execute
### sub method 실행
0
### division : before execute
### division method 실행
### division : afterThrowing execute
### java.lang.ArithmeticException: / by zero : exception occurred

### add : before execute
### add method 실행
### add : after execute
### add : after returning execute
10
### multiply : before execute
### multiply method 실행

보시는것처럼 division 메서드는 ArithmeticException이 발생했기 때문에 @AfterThrowing이 적절하게 발생했지만 multiply 메서드는 그렇지 않은걸 확인하실 수 있습니다.

@Around

@Around는 AOP 중에서도 가장 강력하게 작용하는 Advice입니다. 이 @Around는 다른 어노테이션과는 달리 메서드의 첫번째 인자로 반드시 JoinPoint의 하위타입인 org.aspectj.lang.ProceedingJoinPoint.class 가 반드시 와야 합니다. 메서드의 흐름을 보면 ProceedingJoinPoint.proceed 메서드를 통해 Advice가 적용된 메서드의 실행여부를 @Around내부에서 직접 제어할 수 있습니다.

  • CalculationAspect.class
@Around("execution(* com.sup2is.demo.MyCalculator.sub(..))")
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("### " + joinPoint.getSignature().getName() + 
                       " : before around excute");
    try {
        Object result = joinPoint.proceed();
        return result;
    }finally {
        System.out.println("### " + joinPoint.getSignature().getName() +
                           " : after around excute");
    }
}

메서드의 실행순서는 @Advice가 먼저 실행되고 그 이후에 proceed 메서드가 실행되는데 proceed 메서드의 리턴값이 바로 sub메서드의 return값이 됩니다. 이 말은 @Advice 내부에서 결과값을 제어 할 수도 있다는거죠.

- SpringDemoApplicationTests.class

 

	@Test
	public void subTest() {
		System.out.println(myCalculator.sub(5, 5));
	}

 

 

- console

 

### sub : before around excute
### sub : before execute
### sub method 실행
### sub : after around excute
0

 

이전에 적용했던 @Before와 @Advice에 순서도 확인하실 수 있으시죠?

@Around를 조금 변형해서 만약 결과값이 0이면 -1을 return하도록 수정해보겠습니다.

- CalculationAspect.class

 

	// com.sup2is.demo.MyCalculator class 내부의 sub 메서드에 실행
	@Around("execution(* com.sup2is.demo.MyCalculator.sub(..))")
	public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
		System.out.println("### " + joinPoint.getSignature().getName() +
                           " : before around excute");
		try {
			Object result = joinPoint.proceed();
			
			if(Integer.parseInt(result.toString()) == 0) {
				return -1;
			}
			
			return result;
		}finally {
			System.out.println("### " + joinPoint.getSignature().getName() + "
                               : after around excute");
		}
	}

 

 

- MyCalculator.class

 

	public int sub(int a, int b) {
		System.out.println("### sub method 실행");
		return a - b;
	}

 

 

- console

 

### sub : before around excute
### sub : before execute
### sub method 실행
### sub : after around excute
-1

 

확인하시는것처럼 메서드 내부에는 전혀 수정이 없었지만 @Around 내부에서 값을 제어하는것을 확인하실 수 있습니다.

 

포스팅은 여기까지 하겠습니다. 모든예제는 제 github에서 확인하실 수 있습니다.

 

예제 :https://github.com/sup2is/spring-example/tree/master/springframework-2/src

 

다음시간에는 TDD 관련 해서 포스팅 예정입니다~

퍼가실때는 링크와 출처를 반드시 명시해주세요. 감사합니다.

 

출처 :https://www.javatpoint.com/spring-aop-tutorial

출처https://www.topjavatutorial.com/frameworks/spring/spring-aop/aspect-oriented-programming-concepts/

출처 :https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-api

출처 :http://www.javajigi.net/display/OSS/Aspect-Oriented+Programming+in+Java

출처 :https://www.tutorialspoint.com/spring/aop_with_spring.htm

출처 :https://docs.spring.io/spring/docs/4.0.x/spring-framework-reference/html/aop.html#aop-introduction-defn

출처 :https://howtodoinjava.com/spring-aop/aspectj-around-annotation-example/

 

 

 

원본 : https://sup2is.github.io/spring-framework-2/

 

Spring Framework #2 AOP편

네 .. 저번시간에 DI/IoC 위주로 Spring을 한번 파헤쳐보자고 했는데 역시 블로그가 쉬운게 아니네요 .. 글솜씨가 많이 없지만 양해바랍니다. ㅠㅠ

sup2is.github.io

 

 

 

스프링이란?

스프링은 Rod Johnson이 2002년도에 발표한 ‘Expert One-on-One J2EE Design and Development’ 라는 책에서 발표한 Java 진영의 Framework입니다. 현재 한국 web진영에서 가장 많이 사용한다고 해도 과언이 아닐 정도인데요. Spring은 POJO(plain old Java objects) 기반으로 application을 작성할 수 있도록 도와줍니다. Spring doc 에는 다음과 같은 문장이 있습니다.

 

Make a Java method execute in a database transaction without having to deal with transaction APIs.

Make a local Java method a remote procedure without having to deal with remote APIs.

Make a local Java method a management operation without having to deal with JMX APIs.

Make a local Java method a message handler without having to deal with JMS APIs.

뭐 .. 대충 transaction APIs , remote API … 등등 을 사용하지 않고 java application을 작성할 수 있도록 도와준다는 것 같습니다. 그리고 이게 바로 위에서 언급한 POJO이기도 하구요.

 

저는 스프링을 배울때 3.x 대 부터 사용했었는데 현재 19.03.18 기준으로 mvn repository에 5.1.5 버전이 release 되었네요. 당시에 저도 처음 배울때는 Spring 하면 Web을 가장 먼저 떠올렸었는데 그 외에도 Spring Batch, Spring Cloud 등등 … 아주 많은 곳에서 Spring을 사용하고 있습니다.

 

Spring 하면 대표적으로 떠오르는게 바로 Dependency Injection / Inversion of Control 인데요. 이제 본격적으로 DI/IoC , Spring Architecture 를 한번 파헤치는 시간을 가졌으면 좋겠어요! (저도 공부 할 겸 …)

 

Spring Architecture

Spring 공부하시는 분들은 많이 보셨을만한 그림인데요. Data Access 부터 Web, Core Container 등 다양하게 많습니다. 차근히 알아볼께요!

Dependency Injection / Inversion of Control

제가 생각하는 Spring core의 핵심! 바로 DI/IoC 인데요 사실 스프링 doc을 읽어봐도 DI/IoC 개념이 확 와닿지 않았어요. (저는..) DI/IoC를 그대로 직역하면 DI는 ‘의존성 주입 ‘, IoC는 ‘제어의 역행’ 인데요. DI는 무언가에 의존되어 주입이 되는거고 IoC는 말그대로 제어의 역행을 뜻합니다.

 

제어의 역행을 설명드리면 보통 Java에서 객체를 생성할때 new라는 예약어를 사용해서 객체를 생성하게 되잖아요? 근데 보통 이 new라는 예약어는 개발자가 소스코드에서 직접 선언을 하게 됩니다. 즉 객체를 관리하는 주체가 개발자가 되어요. 만약 개발자가 실수 또는 잘못된 습관으로 A 라는 객체를 두번 생성했다거나 굉장히 큰 프로젝트에서 A라는 객체를 수정해야 할 때, 어디서 A라는 객체를 생성했는지 전부 디버깅해서 하나하나 씩 바꿔주어야 하는 상황이 발생하죠.

 

이런 문제를 개선할 수 있는게 바로 IoC 인데요. Spring에서는 더이상 new 연산자로 객체를 생성하지 않습니다. 바로 이 객체의 생성을 Spring에게 전가시키는건데요. 이것도 생각보다 간단해요. Spring Project가 컴파일러에 의해 Runtime시점으로 들어가기 이전에 Spring은 IoC Container를 통해서 Spring에 사용할 객체들을 초기화시키는 작업을 합니다. 즉 A라는 객체를 Spring Bean에 등록하고 A라는 객체를 사용할때는 IoC Container에서 A라는 객체를 주입시켜서 사용하는건데요. 여기서 말하는 주입이 바로 DI의 개념입니다. (Spring에서 Bean이라는 용어는 객체를 설명하고 있어요)

 

Spring DI/IoC 관련해서 옛날에 봤던 동영상인데 저는 아주 좋게 본 기억이 있어서 같이 공유하고 싶어요

출처 : https://www.youtube.com/watch?v=EERvVf9lNRs

 

Spring DI/IoC를 예제로 아주 훌륭하게 설명해주셨는데 대충 요약해서 설명드리면

“마찬가지로 개발자는 더이상 new 연산자를 사용하지 않고 객체의 생성을 Spring에게 전가시킨다. 이게 IoC, 그리고 Spring Container에서 객체들을 주입받아 사용하는게 바로 DI”

Spring IoC Container를 초기화 시키고 DI를 구현하는 방법은 크게 xml방식, Java Config 방식이 있는데 과거에는 xml방식을 많이 사용했지만 최근에는 Spring Boot에서 Java Config 방식을 지향하고 있기 때문에 유지보수와 신규개발을 동시에 하려면 둘 다 알아야 겠지요..

Example (Spring boot 2.1.3)

- pom.xml

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		
		<dependency>
		    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-starter-web</artifactId>
		</dependency>
	</dependencies>

 

- package 구조

 

- SpringDemoApplication.class

package com.sup2is.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

//@SpringBootApplication 은 @ComponentScan 을 포함하기 때문에 package 기반으로 Bean을 Scan해줍니다.
@SpringBootApplication 
//Spring이 Bean을 초기화할때 xml이 어디있는지 모르니 반드시 명시해줘야 xml방식으로 Bean이 올라가요
@ImportResource({"classpath:spring-context.xml"}) 

public class SpringDemoApplication {

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

}

 

  • Java Config 방식

Spring Bean으로 등록하는 어노테이션은 여러개가 있지만 그중 몇가지만 살펴 볼께요.

- FooComponentA.class

package com.sup2is.demo;

import org.springframework.stereotype.Component;

@Component //Spring이 Bean을 초기화 할 때 @Component 가 붙은 클래스를 확인하고 해당 클래스를 Bean으로 등록
public class FooComponentA {

	public void print() {
		System.out.println(FooComponentA.class.getSimpleName() + " : hello world");
	}
	
}

 

 

- AppConfigration.class

package com.sup2is.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration //마찬가지로 Spring이 Bean을 초기화 할 때 @Configuration 붙은 클래스를 확인
public class AppConfigration {
	
	@Bean  //@Configuration 밑에 @Bean 어노테이션도 Spring Bean으로 등록
	public FooComponentB componentB() {
		return new FooComponentB();
	}

}

 

- FooComponentB.class

package com.sup2is.demo;

public class FooComponentB {

	public void print() {
		System.out.println(FooComponentB.class.getSimpleName() + " : hello world");
	}
}
  • .xml 방식

- spring-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="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">

	<bean id="fooComponentC" class="com.sup2is.demo.FooComponentC" /> <!--Spring Bean으로 등록-->

</beans>

 

 

- FooComponent.class

 

package com.sup2is.demo;

public class FooComponentC {

	public void print() {
		System.out.println(FooComponentC.class.getSimpleName() + " : hello world");
	}
	
}

 

 

 

xml로 등록하는 방식중에는 크게 두가지로 나뉘는데 하는김에 Construcure 주입과 Setter 주입도 한번 알아볼께요.

 

  • .xml 방식 (Constructure)

- spring-context.xml

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="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">

	<!-- <bean id="fooComponentC" class="com.sup2is.demo.FooComponentC"/> --> <!--Spring Bean으로 등록-->
	
	
    <!-- FooComponentC 를 Spring Bean에 등록할 때 name이란 field를 생성자로 초기화-->
	<bean id="fooComponentC" class="com.sup2is.demo.FooComponentC">
		<constructor-arg name="name" value="sup2is demo project"></constructor-arg>
    </bean>
		

</beans>

 

 

- FooComponentC.class

 

package com.sup2is.demo;

public class FooComponentC {

	private String name;
	
    //생성자로 초기화
	public FooComponentC(String name) {
		this.name = name;
	}
	
	public void print() {
		System.out.println(FooComponentC.class.getSimpleName() + " : hello world");
	}
	
	public void printName() {
		System.out.println("my name is " + name);
	}
	
}

 

  • .xml 방식 (Setter)

- spring-context.xml

 

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="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">

	<!-- <bean id="fooComponentC" class="com.sup2is.demo.FooComponentC"/> --> <!--Spring Bean으로 등록-->
	
	<!-- FooComponentC 를 Spring Bean에 등록할 때 name이란 field를 생성자로 초기화-->
	<!-- <bean id="fooComponentC" class="com.sup2is.demo.FooComponentC">
		<constructor-arg name="name" value="sup2is demo project"></constructor-arg>
	</bean>	
	 -->
	 
    <!-- FooComponentC 를 Spring Bean에 등록할 때 name이란 field를 setter 메서드로 초기화-->
	<bean id="fooComponentC" class="com.sup2is.demo.FooComponentC">
		<property name="name" value="sup2is demo project"></property>
	</bean>
		

</beans>

 

- FooComponentC.class

 

package com.sup2is.demo;

public class FooComponentC {

	private String name;
	
    //setter 메서드로 초기화
	public void setName(String name) {
		this.name = name;
	}

	public void print() {
		System.out.println(FooComponentC.class.getSimpleName() + " : hello world");
	}
	
	public void printName() {
		System.out.println("my name is " + name);
	}
	
}

 

  • 테스트

그런데 지금 등록한 이 Spring Bean들은 눈에보이지 않는 메모리에 올라가 있으니 확인할 방법이 없잖아요 그럴때는 DefaultListableBeanFactory.class를 주입받아서 사용해 볼께요. 근데 저희는 DefaultListableBeanFactory.class를 등록한 적이 없는데요? 라고 생각하실 수 도 있는데 Spring에서 기본적으로 초기화되는 Bean들은 여러개가 있어요

 

- SpringDemoApplicationTests.class

 

 

package com.sup2is.demo;

import org.junit.Test;

import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringDemoApplicationTests {
	
    //Spring Bean에 등록된것들중 동일한 타입이 있으면 자동으로 주입해주는 어노테이션
	@Autowired
	private DefaultListableBeanFactory beanFactory;
	
	@Test
	public void beanCheckTest() {
		for(String name : beanFactory.getBeanDefinitionNames()) {
			System.out.println("Bean name : " + beanFactory.getBean(name).getClass().getName());
		}
	}

}

 

 

 

- console

Bean name : org.springframework.context.event.EventListenerMethodProcessor
Bean name : org.springframework.context.event.DefaultEventListenerFactory
Bean name : com.sup2is.demo.SpringDemoApplication$$EnhancerBySpringCGLIB$$ab4a679f
Bean name : org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory
Bean name : com.sup2is.demo.AppConfigration$$EnhancerBySpringCGLIB$$a0500705
Bean name : com.sup2is.demo.FooComponentA
Bean name : com.sup2is.demo.FooComponentB
Bean name : com.sup2is.demo.FooComponentC
...

 

 

저희가 xml과 JavaConfig로 등록한 FooComponent들이 Spring Bean에 등록된걸 확인하실 수 있습니다.

아까 xml로 name값을 초기화 했던 FooComponentC의 printName 메서드도 한번 확인해볼께요

 

- SpringDemoApplicationTests.class

 

package com.sup2is.demo;

import org.junit.Test;

import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringDemoApplicationTests {
	
	@Autowired
	private FooComponentC fooComponentC;

	@Test
	public void fooComponentCTest() {
		fooComponentC.printName();
	}

}

 

 

 

- console

 

2019-03-21 11:27:18.925  INFO 16220 --- [           main] c.s.demo.SpringDemoApplicationTests      : Started SpringDemoApplicationTests in 2.36 seconds (JVM running for 3.5)
my name is sup2is demo project
2019-03-21 11:27:19.181  INFO 16220 --- [       Thread-2] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
...

 

“my name is sup2is demo project” 가 콘솔에 잘 찍히는걸 확인하실 수 있죠?

 

이렇게 Spring Container에 올라간 Bean들은 따로 scope를 정해주지 않는 이상 singleton으로 메모리에 올라가게 되는데 sington은 나중에 알아볼께요 .. 허허

 

포스팅은 여기까지 하겠습니다. 모든예제는 제 github에서 확인하실 수 있습니다.

예제 : https://github.com/sup2is/spring-example/tree/master/springframework-1/src

 

다음시간에는 AOP 관련해서 알아볼께요.

피드백은 언제나 환영입니다 :)

 

퍼가실때는 링크와 출처를 반드시 명시해주세요. 감사합니다.

 

출처 : https://en.wikipedia.org/wiki/Spring_Framework

출처 : https://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/html/overview.html

출처 : https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html

 

 

원본 : https://sup2is.github.io/spring-framework-1/

 

Spring Framework #1 DI/IoC편

스프링이란?

sup2is.github.io

 

 

 

 

 

요즘 클린코드 등등 .. 여러 책을 보면서

 

아 클래스 설계를 어떻게하면 조금 더 좋아질까라는 고민을 많이하다가

 

기능분류로 묶어가면서 해야겠다 라고 생각하던 도중에 찾게된 방법 ..

 

 

나같은 경우는 enum에서

 

@Autowired로 받아야되는 필드가있는데

 

어캐해야될지 몰랐었음

 

 

 

아래는 방법

 

 

public enum ReportType {

    REPORT_1("name", "filename"),
    REPORT_2("name", "filename");

    @Component
    public static class ReportTypeServiceInjector {
        @Autowired
        private DataPrepareService dataPrepareService;

        @PostConstruct
        public void postConstruct() {
            for (ReportType rt : EnumSet.allOf(ReportType.class))
               rt.setDataPrepareService(dataPrepareService);
        }
    }

[...]

}

 

 

글에서는 ReportTypeServiceInjector 를 public으로 선언하지만

 

나는 클래스 내부에서 한정적으로 private로 선언했다

 

 

 

그리고 enum class에서 @Autowired 받을 필드의 setter 메서드를 다음과 같이 만들었다

private static void set( ... field ) {
  //this.field = field
}

 

 

그리고 set 메서드를 postConstruct() 메서드 안에서 호출했다.

 

 

이런식으로 하면

 

enum 클래스에 @Component와 @Autowired 필드를 따로 두지 않고

inner 클래스를 활용해서 field를 set할 수 있당..

 

 

 

 

 

출처 : https://stackoverflow.com/questions/16318454/inject-bean-into-enum/39097926

이 블로그에 글이 없으면 아주 좋은일이다...

 

왜냐면 대부분 쉽게 처리하고 삽질 안한거니까 ...

 

 

Spring boot에서 pom.xml에 외부 library 추가는 다음과 같이 한다.

 

<dependency>
	<groupId>DaouCrypto-20180824</groupId>
	<artifactId>DaouCrypto-20180824</artifactId>
	<version>1.0</version>
	<scope>system</scope>
	<systemPath>${basedir}/src/main/resources/external-lib/DaouCrypto-20180824.jar</systemPath>
</dependency>

 

scope가 system일때 systemPath property를 사용 가능한데

반드시 절대경로여야한다

classpath 안됨

외부 jar 추가하고 build까지는 잘 됐는데

runtime에서 NoClassDefFoundError를 뱉었다.

아래는 해결~

 

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <includeSystemScope>true</includeSystemScope>
  </configuration>
</plugin>

 

plugin에

  <configuration>
    <includeSystemScope>true</includeSystemScope>
  </configuration>

 

를 추가해주면 빌드도 잘 되고 java -jar ... 로 spring boot server를 올려도 아주 잘 작동된다.

 

 

 

출처 :https://stackoverflow.com/questions/30207842/add-external-library-jar-to-spring-boot-jar-internal-lib





꼬박 하루를 날렸다 ...


왜 autowired가 안되는지 이유를 몰라서 다른곳에서 너무 삽질했다




좀 더 자세하게 찾아봐야하지만



@Autowired will not work in a Quartz job implementation because it will not be instantiated by Spring ... 인걸 보면


Quartz 랑 Spring 사이에 무언가 있는 듯 하다 ...



같은 내용으로 검색하니 다른 개발자분들도 많은 블로그를 썼지만 java config는 별로 없기에...





Bean 등록할 때 다음과같이 setApplicationContextSchedulerContextKey("applicationContext"); 를 명시해준다


1
2
3
4
5
6
7
8
9
10
@Bean
public jaSchedulerFactoryBean schedulerFactoryBean() throws IOException, SchedulerException
{
    SchedulerFactoryBean scheduler = new SchedulerFactoryBean();
    scheduler.setTriggers(jobOneTrigger(), jobTwoTrigger());
    scheduler.setQuartzProperties(quartzProperties());
    scheduler.setJobDetails(jobOneDetail(), jobTwoDetail());
    scheduler.setApplicationContextSchedulerContextKey("applicationContext");
    return scheduler;
}
cs







setApplicationContextSchedulerContextKey API 문서를 보면





public void setApplicationContextSchedulerContextKey(java.lang.String applicationContextSchedulerContextKey)


Set the key of an ApplicationContext reference to expose in the SchedulerContext, for example "applicationContext". Default is none. Only applicable when running in a Spring ApplicationContext.

Note: When using persistent Jobs whose JobDetail will be kept in the database, do not put an ApplicationContext reference into the JobDataMap but rather into the SchedulerContext.


In case of a QuartzJobBean, the reference will be applied to the Job instance as bean property. An "applicationContext" attribute will correspond to a "setApplicationContext" method in that scenario.


Note that BeanFactory callback interfaces like ApplicationContextAware are not automatically applied to Quartz Job instances, because Quartz itself is responsible for the lifecycle of its Jobs.


See Also:

JobDetailFactoryBean.setApplicationContextJobDataKey(java.lang.String), ApplicationContext



라고 되어있다




Job을 구현한 모든 클래스는 execute method를 구현하는데 메서드에서 넘어온 JobExecutionContext를 통해 Spring의 ApplicationContext를 찾는다



1
2
3
4
5
6
7
8
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
    ApplicationContext applicationContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
 
    BeanObject bean = applicationContext.getBean(BeanObject.class); 
 
    //bla bla...
}
cs





ㅠㅠㅠㅠㅠㅠㅠㅠㅠ 힘들었다





https://stackoverflow.com/questions/25719179/quartz-does-not-support-autowired

https://stackoverflow.com/questions/25719179/quartz-does-not-support-autowired

https://stackoverflow.com/questions/25719179/quartz-does-not-support-autowired



https://howtodoinjava.com/spring-batch/spring-beans-in-quartz-job/

https://howtodoinjava.com/spring-batch/spring-beans-in-quartz-job/

https://howtodoinjava.com/spring-batch/spring-beans-in-quartz-job/





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import org.quartz.*;
import org.quartz.impl.*;
 
public abstract class ChainableJob implements Job {
   private static final String CHAIN_JOB_CLASS = "chainedJobClass";
   private static final String CHAIN_JOB_NAME = "chainedJobName";
   private static final String CHAIN_JOB_GROUP = "chainedJobGroup";
   
   @Override
   public void execute(JobExecutionContext context) throws JobExecutionException {
      // execute actual job code
      doExecute(context);
 
      // if chainJob() was called, chain the target job, passing on the JobDataMap
      if (context.getJobDetail().getJobDataMap().get(CHAIN_JOB_CLASS) != null) {
         try {
            chain(context);
         } catch (SchedulerException e) {
            e.printStackTrace();
         }
      }
   }
   
   // actually schedule the chained job to run now
   private void chain(JobExecutionContext context) throws SchedulerException {
      JobDataMap map = context.getJobDetail().getJobDataMap();
      @SuppressWarnings("unchecked")
      Class jobClass = (Class) map.remove(CHAIN_JOB_CLASS);
      String jobName = (String) map.remove(CHAIN_JOB_NAME);
      String jobGroup = (String) map.remove(CHAIN_JOB_GROUP);
      
      
      JobDetail jobDetail = newJob(jobClass)
            .withIdentity(jobName, jobGroup)
            .usingJobData(map)
            .build();
         
      Trigger trigger = newTrigger()
            .withIdentity(jobName + "Trigger", jobGroup + "Trigger")
                  .startNow()      
                  .build();
      System.out.println("Chaining " + jobName);
      StdSchedulerFactory.getDefaultScheduler().scheduleJob(jobDetail, trigger);
   }
 
   protected abstract void doExecute(JobExecutionContext context) 
                                    throws JobExecutionException;
   
   // trigger job chain (invocation waits for job completion)
   protected void chainJob(JobExecutionContext context, 
                          Class jobClass, 
                          String jobName, 
                          String jobGroup) {
      JobDataMap map = context.getJobDetail().getJobDataMap();
      map.put(CHAIN_JOB_CLASS, jobClass);
      map.put(CHAIN_JOB_NAME, jobName);
      map.put(CHAIN_JOB_GROUP, jobGroup);
   }
}
cs






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;
import org.quartz.*;
 
public class TestJob extends ChainableJob {
 
   @Override
   protected void doExecute(JobExecutionContext context) 
                                   throws JobExecutionException {
      JobDataMap map = context.getJobDetail().getJobDataMap();
      System.out.println("Executing " + context.getJobDetail().getKey().getName() 
                         + " with " + new LinkedHashMap(map));
      
      boolean alreadyChained = map.get("jobValue"!= null;
      if (!alreadyChained) {
         map.put("jobTime"new Date().toString());
         map.put("jobValue"new Random().nextLong());
      }
      
      if (!alreadyChained && new Random().nextBoolean()) {
         chainJob(context, TestJob.class"secondJob""secondJobGroup");
      }
   }
   
}
cs





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.quartz.*;
import org.quartz.impl.*;
 
public class Test {
   
   public static void main(String[] args) throws Exception {
 
      // start up scheduler
      StdSchedulerFactory.getDefaultScheduler().start();
 
      JobDetail job = JobBuilder.newJob(TestJob.class)
             .withIdentity("firstJob""firstJobGroup").build();
 
      // Trigger our source job to triggers another
      Trigger trigger = TriggerBuilder.newTrigger()
            .withIdentity("firstJobTrigger""firstJobbTriggerGroup")
            .startNow()
            .withSchedule(
                  SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1)
                  .repeatForever()).build();
 
      StdSchedulerFactory.getDefaultScheduler().scheduleJob(job, trigger);
      Thread.sleep(5000);   // let job run a few times
 
      StdSchedulerFactory.getDefaultScheduler().shutdown();
   }
   
}
cs







https://dzone.com/articles/job-chaining-quartz-and

https://dzone.com/articles/job-chaining-quartz-and

https://dzone.com/articles/job-chaining-quartz-and



아주 훌륭한 예제입니다 굿잡




http://bbokkun.tistory.com/141

http://bbokkun.tistory.com/141

http://bbokkun.tistory.com/141


+ Recent posts