4. Architecture

4.1 Controller

4.1.1 Unit Test

EntityController 의 테스트 코드는 다음과 같다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="file:src/main/webapp/WEB-INF/spring/root-context.xml")
@WebAppConfiguration
public class ControllerTest{

	protected final Logger logger = LoggerFactory.getLogger(getClass());

	@Autowired
	protected WebApplicationContext applicationContext;

	protected MockMvc mockMvc;

	@Before
	public void setup() throws Exception {
		if(mockMvc == null){
			this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build();
		}
		super.setup();
	}

	@Test
	public void testMethod() throws Exception{
		this.mockMvc.perform(
				get("/foo/find")
			)
			.andDo(print())
			.andExpect(status().isOk());
	}
}
			

4.2 Validator

4.2.1 Override

Section 1.4, “Springfield Example” 에서 자동 생성하는 Bean 중 Validator 레이어에 에 해당하는 "fooValidator" Bean 을 AbstractEntityValidator 을 extends 한 MyFooValidator 와 <context:component-scan> 을 사용하여 교체할수 있다.

package com.yourcompany.yourproject.foo;
                                                                                                                       (1)
@Component("fooValidator")
public class MyFooValidator extends AbstractEntityValidator<Foo, Foo>{

	@Autowired
	@Qualifier("fooRepository")
	private EntityRepository<Foo, String> fooRepository;

	@Override
	@Transactional
	public void create(ExtendsBean target, Errors errors) {
		//your validation logic...                                                                                           (2)
	}
}
			

1

등록 되는 MyFooValidator 의 Bean Name 은 Section 1.3, “<springfield:modules>” 를 에 따라 "{@Springfield class short name}Validator" 가 되어야 한다.

3

validation logic 을 위해 DAO 가 필요하다면, Bean 을 주입받는다. 해당 메소드에 @Transactional 을 선언하면 Transaction 이 가능하다.

<beans>
	<jdbc:embedded-database id="yourDataSource" type="HSQL"/>  	

	<context:component-scan base-package="com.yourcompany.yourproject"/>                                                  (1)
	
	<springfield:modules base-package="com.yourcompany.yourproject" 
			data-source-ref="yourDataSource"
	/>
	...
</beans>
			

1

Bean 을 교체하기 위해 <springfield:modules> 보다 앞서 <context:component-scan> 으로 MyFooValidator 를 등록 하여야 한다.

4.3 Service

4.3.1 Override

Section 1.4, “Springfield Example” 에서 자동 생성하는 Bean 중 Service 레이어에 에 해당하는 "fooService" Bean 을 AbstractEntityService 을 extends 한 MyFooService 와 <context:component-scan> 을 사용하여 교체할수 있다.

package com.yourcompany.yourproject.foo;
			
@Service("fooService")                                                                                                 (1)
public class MyFooService extends AbstractEntityService<Foo, Foo>{

	@Autowired
	@Qualifier("fooRepository")
	private EntityRepository<Foo, String> fooRepository;

	@Autowired 
	private TransactionTemplate transactionTemplate;
	
	@Override
	protected EntityRepository<Foo, String> getRepository() {                                                             (2)
		return fooRepository;
	}

	@Override
	protected TransactionTemplate getTransactionTemplate() {                                                              (2)
		return transactionTemplate;
	}
	
	@Override
	public Foo create(Foo entity) {
		return getTransactionTemplate().execute(new TransactionCallback<Foo>() {
			public ExtendsBean doInTransaction(TransactionStatus status) {
				
				//your service logic..	                                                                                            (3)

				ExtendsBean result = getRepository().save(entity);
				return result;
			}
		});
	}
	
	@Override
	@Transactional
	public Foo read(Foo entity) {
		
		//your service logic..	                                                                                              (4)

		ExtendsBean result = getRepository().save(entity);
		return result;
	}
}
			

1

등록 되는 레이어의 Bean Name 은 Section 1.3, “<springfield:modules>” 를 에 따라 "{@Springfield class short name}Service" 가 되어야 한다.

2

MyFooService 의 기본 동작을 위해, getRepository() 와 getTransactionTemplate() 를 override 하여야 한다.

3

getTransactionTemplate() 을 이용하여 Transaction 을 수행 할 수 있다.

4

@Transactional을 이용하여 Transaction 을 수행 할 수 있다.

<beans>
	<jdbc:embedded-database id="yourDataSource" type="HSQL"/>  	

	<context:component-scan base-package="com.yourcompany.yourproject"/>                                                  (1)
	
	<springfield:modules base-package="com.yourcompany.yourproject" 
			data-source-ref="yourDataSource"
	/>
	...
</beans>
			

1

Bean 을 교체하기 위해 <springfield:modules> 보다 앞서 <context:component-scan> 으로 MyFooService 를 등록 하여야 한다.

4.3.2 DTO

Section 1.2, “@Springfield” 의 strategy 속성이 Strategy.DTO 인 경우 Repository 레이어가 생성되지 않는다. 따라서 아무 동작도 하지 않는 Service 레이어 가 생성된다. 이 경우 Service 레이어 는 EntityService 를 implements 한 Bean 으로 교체하여야 한다.

[Important]Important
Strategy.DTO 방식은 WebMVC 의 요청파라미터 Command 객체와 도메인 객체를 분리하기 위해 사용되는 방식이다.
package com.yourcompany.yourproject.dto;
		
@Springfield(
	strategy=Strategy.DTO,                                                                                                (1)
	identity="param1")
public class DtoBean {

	private String param1;
	private Integer param2;
	
	//...
}
			

1

Strategy.DTO 인 경우 RESTful URL 구성을 위해 identity 속성을 정의 하여야 한다.

package com.yourcompany.yourproject.dto;

@Service("dtoBeanService")
public class DtoBeanService implements EntityService<DtoBean, DtoBean>{

	@Autowired @Qualifier("fooRepository")
	private EntityRepository<Foo, String> fooRepository;

	@Autowired @Qualifier("barRepository")
	private EntityRepository<Bar, Integer> barRepository;
	
	@Override
	@Transactional
	public DtoBean create(DtoBean entity) {                                                                               (1)

		Foo foo = new Foo();
		foo.setName(entity.getParam1());
		foo.setAge(entity.getParam2());
		fooRepository.save(foo);

		Bar bar = new Bar();
		bar.setSeq(entity.getParam2());
		bar.setDesc(entity.getParam1());
		barRepository.save(bar);
		
		return entity;
	}

	....
}
			

1

@Transactional 선언으로 transaction 을 수행하고 Repository 레이어를 주입받아서 서비스 로직을 구현할수 있다.

<beans>
	<jdbc:embedded-database id="yourDataSource" type="HSQL"/>  	

	<context:component-scan base-package="com.yourcompany.yourproject"/>                                                  (1)
	
	<springfield:modules base-package="com.yourcompany.yourproject" 
			data-source-ref="yourDataSource"
	/>
	...
</beans>
			

1

Bean 을 교체하기 위해 <springfield:modules> 보다 앞서 <context:component-scan> 으로 MyFooService 를 등록 하여야 한다.

4.4 Repository

4.4.1 Unit Test

EntityRepository 의 테스트 코드는 다음과 같다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Transactional
public class RepositoryTest{

	protected final Logger logger = LoggerFactory.getLogger(getClass());

	@Autowired
	@Qualifier("fooRepository")
	private EntityRepository<Foo, String> fooRepository;

	@Autowired
	@Qualifier("barRepository")
	private EntityRepository<Bar, Integer> barRepository;

	@Test
	public void testMethod() throws Exception{
		//Test Code...
	}
}
			

4.4.2 Query Keyword

QueryMethod Argument 란 EntityRepository 메소드의 argument 를 의미한다.

public interface EntityRepository<T, ID extends Serializable> {

	...
		
	public long deleteAll(Object queryMethod);
	public long count(Object queryMethod);
	public List<T> findAll(Object queryMethod);
	public List<T> findAll(Object queryMethod, Sort sort);
	public Page<T> findAll(Object queryMethod, Pageable pageable);
	
}
		

Query Keyword 는 QueryMethod Argument 의 Class Short Name 을 의미한다. Query Keyword 는 @QueryMethod 을 이용하여 변경 할 수 있다. QueryKeyword 는 "FindBy" 로 시작하며, Spring-data 의 Query keyword 규칙을 따른다.

public class FindByNameAndAgeOrderByAgeAsc{
	private String name;
	private Integer age;
}

@QueryMethod("findByNameAndAgeOrderByAgeAsc")
public class MyQuery{
	private String name;
	private Integer age;
}
		

QueryMethod Argument 는 Query Keyword 에 따라 필여한 property 를 선언하여야 한다. property 값의 null 여부에 따라 동적으로 where 조건을 바꿀수 있다.

  • select * from Person
    				//Test Code...
    				
    				FindByNameAndAge queryMethod1 = new FindByNameAndAge();
    				//or 
    				//MyQuery queryMethod1 = new MyQuery();
    		
    				List<Person> result1 = fooRepository.findAll(queryMethod1);
    				Assert.assertEquals(result1.size(), 3);
    				
  • select * from Person where name = 'a'
    				//Test Code...
    
    				FindByNameAndAge queryMethod2 = new FindByNameAndAge();
    				//or 
    				//MyQuery queryMethod2 = new MyQuery();
    				
    				queryMethod2.setName("a");
    		
    				List<Person> result2 = fooRepository.findAll(queryMethod2);
    				Assert.assertEquals(result2.size(), 1);
    				
  • select * from Person where name = 'a' and age = 1
    				//Test Code...
    
    				FindByNameAndAge queryMethod3 = new FindByNameAndAge();
    				//or 
    				//MyQuery queryMethod3 = new MyQuery();
    
    				queryMethod3.setName("a");
    				queryMethod3.setAge(1);
    		
    				List<Person> result3 = fooRepository.findAll(queryMethod3);
    				Assert.assertEquals(result3.size(), 1);
    				

4.4.3 TemplateCallback

TemplateCallback 을 이용하면 EntityRepository 에서 로우레벨의 ORM 관련 객체에 바로 접근 할 수 있다.

public interface EntityRepository<T, ID extends Serializable> {

	...
		
	public <R, X> R execute(TemplateCallback<R, X> callback);
	
}
		

다음표는 자동 생성되는 EntityRepository 의 구현체별로 사용가능한 TemplateCallback 의 Generic Type 을 나타낸다.

Table 4.1.  TemplateCallback Generic Type

Repository Implementation TemplateCallback
JpaRepository TemplateCallback<R, javax.persistence.EntityManager>
HibernateRepository TemplateCallback<R, org.hibernate.Session>
SqlSessionRepository TemplateCallback<R, org.mybatis.spring.SqlSession>
JdbcRepository TemplateCallback<R, org.springframework.jdbc.core.JdbcTemplate>
TemplateCallback<R, java.sql.Connection>

  • working with javax.persistence.EntityManager instance
    				//Test Code...
    				List<Foo> result2 = fooRepository.execute(new TemplateCallback<List<Foo>, EntityManager>() {
    					public List<Foo> doInTemplate(EntityManager em) {
    		
    						//Using QueryDsl
    						Foo alias = Alias.alias(Foo.class, "foo");
    						EntityPath<Foo> foo = Alias.$(alias);
    						StringPath fooName = Alias.$(alias.getName());
    						NumberPath<Integer> fooAge = Alias.$(alias.getAge());
    						
    						JPAQuery query = new JPAQuery(em);
    						query.from(foo);
    						query.where(fooName.eq("a"));
    						query.where(fooAge.eq(1));
    		
    						return query.list(foo);
    					}
    				});
    				Assert.assertEquals(result2.size(), 1);
    				
  • working with org.hibernate.Session instance
    				//Test Code...
    				List<Foo> result2 = fooRepository.execute(new TemplateCallback<List<Foo>, Session>() {
    		
    					@Override
    					public List<Foo> doInTemplate(Session session) {
    		
    						//Using Hibernate Criteria
    						Criteria criteria = session.createCriteria(Foo.class);
    						criteria.add(Restrictions.eq("name", "a"));
    						criteria.add(Restrictions.eq("age", 1));
    						return criteria.list();
    					}
    				});
    				Assert.assertEquals(result2.size(), 1);
    				

4.5 Multipart

4.5.1 Unit Test

Section 1.3, “<springfield:modules>” 가 제공하는 MultipartFileHandler 는 File Upload , Download 를 지원한다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="file:src/main/webapp/WEB-INF/spring/root-context.xml")
public class MultipartFileHandlerTest {
			
	@Autowired
	protected MultipartFileHandler multipartFileHandler;
	
	@Test
	public void test() throws Exception {

		String contentFile1 = multipartFileHandler.uploadFile(f);                                                            (1)
		String contentFile2 = multipartFileHandler.uploadFile(f, new UploadFileNameResolver() {                              (2)
			@Override
			public String resolveFileName(MultipartFile multipartFile) throws IOException {
				return "Your Prefix"+multipartFile.getOriginalFilename()+"Your Suffix";
			}
		});

		File uploadedFile = multipartFileHandler.findFile(contentFile1);                                                     (3)
		Assert.assertEquals(true, uploadedFile.exists());

		multipartFileHandler.deleteFile(contentFile1);
		Assert.assertEquals(false, uploadedFile.exists());                                                                   (4)
	}
			

1

지정돤 경로로 업로드하고 contentFile(저장된 파일의 이름)을 리턴한다. See ???

contentFile 은 디폴트로 {System.currentTimeMillis}_{Original Filename} 로 결정된다.

2

UploadFileNameResolver 를 사용하여 contentFile 을 변경 할수 있다.

3

contentFile 로 물리적인 파일 객체를 리턴받을 수 있다.

4

contentFile 로 물리적인 파일을 삭제 할 수 있다.

4.5.2 Upload / Download

다음은 Service 레이어를 변경하여 파일을 업로드 하고 메타정보를 데이터 베이스에 저장하고, 업로드된 파일을 리스트업하고 다운로드하는 예제이다.

package com.yourcompany.yourproject.file;

@Springfield(
	methodLevelMapping={"find", "createForm", "create", "read.download","read.stream"})                                   (1)
@Entity
public class FileBean implements MultipartFileBean{                                                                    (2)

	@Id
	public String contentFile;
	public String contentName;
	public String contentType;
	public Long contentSize;
	
	@Transient 
	public MultipartFile uploadFile;                                                                                      (3)
	
	//...
}
			

1

[GET:/file/{contentName}.download] 또는 [GET:/file/{contentName}.stream] 요청일 경우 MultipartFileHandler .findFile() 을 이용하여 미리 저장 또는 업로드된 파일을 Downloading 하거나 Streaming 한다.

2

Section 1.3, “<springfield:modules>” 가 지원하는 확장자중, *.download , *.stream 은 도메인 객체가 MultipartFileBean 을 implements 한 경우에만 동작가능하다. MultipartFileBean 의 getContentFile() 값으로 MultipartFileHandler .findFile() 을 이용하여 미리 저장 또는 업로드된 파일을 Downloading 하거나 Streaming 한다.

3

<form method="POST" enctype="multipart/form-data"> 의 <input type="file" name="uploadFile"> 를 도메인 객체에 담아두기 위해 필요한 property 이다. uploadFile 은 데이터베이스에 저장하지 않는 필드이므로 @javax.persistence.Transient 가 선언되었다.

package com.yourcompany.yourproject.file;

@Service("fileBeanService")
public class FileBeanService extends AbstractEntityService<FileBean,FileBean>{

	@Autowired @Qualifier("fileBeanRepository")
	private EntityRepository<FileBean, String> fileBeanRepository;

	@Autowired 
	private TransactionTemplate transactionTemplate;
	
	@Override
	protected EntityRepository<FileBean, String> getRepository() {
		return fileBeanRepository;
	}

	@Override
	protected TransactionTemplate getTransactionTemplate() {
		return transactionTemplate;
	}
	
	@Autowired 
	private MultipartFileHandler multipartFileHandler;
	
	@Override
	public FileBean create(FileBean entity) {                                                                             (1)
		
		MultipartFile f = entity.getUploadFile();
		try {
			String contentFile = multipartFileHandler.uploadFile(f);
			String contentName = f.getOriginalFilename();
			String contentType = f.getContentType();
			Long contentSize = f.getSize();
		
			entity.setContentFile(contentFile);
			entity.setContentName(contentName);
			entity.setContentType(contentType);
			entity.setContentSize(contentSize);
			
			return super.create(entity);

		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
}
			

1

MultipartFileHandler 를 이용하여 파일을 업로드하고 메타정보를 도매인 객체에 담아서 이를 데이터베이스에 저장한다.
<beans>
	<jdbc:embedded-database id="yourDataSource" type="HSQL"/>  	

	<context:component-scan base-package="com.yourcompany.yourproject"/>                                                  (1)
	
	<springfield:modules base-package="com.yourcompany.yourproject" 
			data-source-ref="yourDataSource"
	/>
	...
</beans>
			

1

Bean 을 교체하기 위해 <springfield:modules> 보다 앞서 <context:component-scan> 으로 FileBeanService 를 등록 하여야 한다.

4.6 Security

4.6.1 Config

Section 1.3, “<springfield:modules>” spring security 관련 설정값을 properties-ref 로 변경 할 수 있다. properties-ref 가 선언되지 않은 경우 디폴트 값이 사용된다.

<beans>

	<util:properties id="yourProp" 
		location="classpath:com/yourcompany/yourproject/config.properties" />


	<jdbc:embedded-database id="yourDataSource" type="HSQL"/>  	
	
	<springfield:modules base-package="com.yourcompany.yourproject" 
			data-source-ref="yourDataSource"
			properties-ref="yourProp"
	/>
	...
</beans>
		
#
# com/yourcompany/yourproject/config.properties
#
springfield.security.formPage=/security/user/loginForm.html
springfield.security.formUsername=j_username
springfield.security.formPassword=j_password
springfield.security.formRememberme=_spring_security_remember_me
springfield.security.loginUrl=/j_spring_security_check
springfield.security.logoutUrl=/j_spring_security_logout
		

4.6.2 UserDetails

다음은 사용자 계정을 데이터베이스로 관리하는 예제이다.

package com.yourcompany.yourproject.security;
		
@Springfield
@Entity
public class User implements org.springframework.security.core.userdetails.UserDetails{                                (1)

	@Id 
	private String username;
	private String password;
	private boolean enabled = true;
	private boolean accountNonExpired = true;
	private boolean accountNonLocked = true;
	private boolean credentialsNonExpired = true;
	private String salt;
	private String role;
	
	//getter / setter

	@Transient
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return Role.valueOf(role).getAuthorities();
	}
	
	public enum Role {                                                                                                    (2)

		USER(new SimpleGrantedAuthority("ROLE_ANONYMOUS"), new SimpleGrantedAuthority("ROLE_USER")),
		ADMIN(new SimpleGrantedAuthority("ROLE_ANONYMOUS"), new SimpleGrantedAuthority("ROLE_USER"), new SimpleGrantedAuthority("ROLE_ADMIN"));
		
		private Collection<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();

		Role(GrantedAuthority... authorities){
			for(GrantedAuthority authority : authorities){
				grantedAuthorities.add(authority);
			}
		}
		public Collection<? extends GrantedAuthority> getAuthorities(){
			return grantedAuthorities;
		}
	}
}
			

1

사용자 계정을 데이터베이스로 관리하기 위해 org.springframework.security.core.userdetails.UserDetails 를 implements 한 도메인 객체가 필요하다.

2

권한을 가진 사용자 ROLE 을 필요에 따라 정의한다.

package com.yourcompany.yourproject.security;
	
@Service("userService")                                                                                                (1)
public class UserService extends AbstractEntityService<User, User>{

	@Autowired
	private AuthenticationManager authenticationManager;

	@Autowired
	private PasswordEncoder passwordEncoder; 
	
	@Autowired
	private SaltSource saltSource; 

	@Autowired @Qualifier("userRepository")
	private EntityRepository<User, String> userRepository;

	@Autowired 
	private TransactionTemplate transactionTemplate;
	
	@Override
	protected EntityRepository<User, String> getRepository() {
		return userRepository;
	}

	@Override
	protected TransactionTemplate getTransactionTemplate() {
		return transactionTemplate;
	}
	
	@Override
	@Transactional
	public User create(User entity) {

		String salt = ""+System.currentTimeMillis();
		String password = passwordEncoder.encodePassword("password", salt);                                                  (2)
		
		entity.setSalt(salt);
		entity.setPassword(password);
		entity.setRole("USER");
		
		return super.create(entity);
	}
}
			

1

등록 되는 Service 레이어의 Bean Name 은 Section 1.3, “<springfield:modules>” 를 에 따라 "{@Springfield class short name}Service" 가 되어야 한다.

2

암호화된 패스워드와 password salt 를 저장한다.

<beans>
	<jdbc:embedded-database id="yourDataSource" type="HSQL"/>  	

	<context:component-scan base-package="com.yourcompany.yourproject"/>                                                  (1)
	
	<springfield:modules base-package="com.yourcompany.yourproject" 
			data-source-ref="yourDataSource"
	/>
	...
</beans>
			

1

Bean 을 교체하기 위해 <springfield:modules> 보다 앞서 <context:component-scan> 으로 UserService 를 등록 하여야 한다.

4.6.3 UserDetailsService

org.springframework.security.core.userdetails.UserDetailsService 를 구현한 Bean 이 등록되어 있다면, Section 1.3, “<springfield:modules>” 이 동적으로 이 Bean 을 추적하여 spring security 가 로그인을 처리하도록 설정한다. org.springframework.security.web.authentication.AuthenticationSuccessHandler 를 구현한 Bean 이 등록되어 있다면, 로그인 성공 이벤트를 수신할수 있으며, org.springframework.security.web.authentication.AuthenticationFailureHandler 를 구현한 Bean 이 등록되어 있다면, 로그인 실패 이벤트를 수신할수 있으며,

package com.yourcompany.yourproject.security;
		
@Component
public class LoginService implements org.springframework.security.core.userdetails.UserDetailsService{

	@Autowired @Qualifier("userRepository")
	private EntityRepository<User, String> userRepository;

	@Override
	@Transactional
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userRepository.findOne(username);                                                                        (1)
		if (user == null) {
			throw new DataRetrievalFailureException("Query returned no results for user '" + username + "'");
		}
		return user;
	}
}
			

1

사용자 계정을 데이터베이스에서 조회하여 로그인 처리 한다.

package com.yourcompany.yourproject.security;
		
@Component
public class LoginSuccess implements org.springframework.security.web.authentication.AuthenticationSuccessHandler{

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, 
		HttpServletResponse response, 
		Authentication authentication) throws IOException, ServletException {                                                (1)
		//로그인 성공 이벤트 
	}
}
			

1

로그인 성공 이벤트를 수신하여 로직을 구현할수 있다.

package com.yourcompany.yourproject.security;
		
@Component
public class LoginFailure implements org.springframework.security.web.authentication.AuthenticationSuccessHandler{

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, 
		HttpServletResponse response, 
		AuthenticationException exception)throws IOException, ServletException {                                             (1)
		//로그인 실패 이벤트 
	}
}
			

1

로그인 실패 이벤트를 수신하여 로직을 구현할수 있다.

<beans>
	<jdbc:embedded-database id="yourDataSource" type="HSQL"/>  	

	<context:component-scan base-package="com.yourcompany.yourproject"/>                                                  (1)
	
	<springfield:modules base-package="com.yourcompany.yourproject" 
			data-source-ref="yourDataSource"
	/>
	...
</beans>
			

1

<context:component-scan> 으로 LoginService , LoginSuccess , LoginFailure 를 등록 하여야 한다.