'개발관련'에 해당되는 글 51건
- 2015.12.30 :: Spring Security를 이용한 login(2)
- 2015.12.30 :: Spring Security를 이용한 login(1) 1
- 2015.12.23 :: jpa ManyToMany에 Column 추가하기
- 2015.12.17 :: hibernate(10)
- 2015.12.07 :: hibernate(9)
- 2015.12.07 :: hibernate(8)
- 2015.12.07 :: hibernate(7)
- 2015.12.07 :: hibernate(6)
- 2015.12.04 :: hibernate(5)
- 2015.12.04 :: hibernate(4)
이제 코드를 살펴보자.
먼저 jpa로 person이라는 객체를 만들어 보자.
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | package com.gno.sample.dto; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity(name="person") public class Person { private int idx; private String id; private String email; private String password; private String auth; public Person() { // TODO Auto-generated constructor stub } public Person(String id, String email, String password) { // TODO Auto-generated constructor stub this.id = id; this.email = email; this.password = password; } @Id @GeneratedValue(strategy = GenerationType.AUTO) public int getIdx() { return idx; } public void setIdx(int idx) { this.idx = idx; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getAuth() { return auth; } public void setAuth(String auth) { this.auth = auth; } @Override public String toString() { // TODO Auto-generated method stub return "name=" + id + ", password=" + password + ", email=" + email; } } | cs |
idx는 auto generator 로 auth는 권한을 저장하는 컬럼 나머진 이름 그대로 .... repository를 생략한다.
먼저 provider를 만들자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package com.gno.sample.security; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; public class CustomAuthenticationProvider extends DaoAuthenticationProvider { private final Log logger = LogFactory.getLog(this.getClass()); @Override public Authentication authenticate(Authentication arg0) throws AuthenticationException { // TODO Auto-generated method stub return super.authenticate(arg0); } } | cs |
위와 같이 선언하면 내부적으로 알아서 form에서 넘어온 데이터(Authentication은 form에 넘어온 정보를 저장하고 있다.)와 db에서 가져온 정보를 비교해준다. 상위 클래스 소스를 보면 내부적으로 무언가 비교를 해주고 있다. 그래서 override만 해주면 된다.
다음은 service 코드를 보자.
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 | package com.gno.sample.security; import java.util.ArrayList; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import com.gno.sample.dto.Person; import com.gno.sample.repository.PersonRepository; public class CustomUserDetailService implements UserDetailsService{ @Autowired private PersonRepository repo; @Override public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException { // TODO Auto-generated method stub ArrayList<Person> person = (ArrayList<Person>) repo.findById(id); if(person.size() == 0){ throw new UsernameNotFoundException(id); } //userDetail을 default로 사용할때 // return new User(person.get(0).getId(), person.get(0).getPassword(), "ROLE_USER"); return new CustomUserDetail(person.get(0)); } } | cs |
service는 단순히 form에서 넘어온 id를 repository에서 가져와 userDetail에 넘겨준다.
마지막으로 UserDetail을 보자.
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 62 63 64 65 66 67 68 69 70 | package com.gno.sample.security; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import com.gno.sample.dto.Person; public class CustomUserDetail implements UserDetails{ private Person person; public CustomUserDetail(Person person) { // TODO Auto-generated constructor stub this.person = person; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { // TODO Auto-generated method stub List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); authorities.add(new SimpleGrantedAuthority(person.getAuth())); return authorities; } @Override public String getPassword() { // TODO Auto-generated method stub return this.person.getPassword(); } @Override public String getUsername() { // TODO Auto-generated method stub return this.person.getId(); } @Override public boolean isAccountNonExpired() { // TODO Auto-generated method stub return true; } @Override public boolean isAccountNonLocked() { // TODO Auto-generated method stub return true; } @Override public boolean isCredentialsNonExpired() { // TODO Auto-generated method stub return true; } @Override public boolean isEnabled() { // TODO Auto-generated method stub return true; } } | cs |
UserDetail은 override 함수에 jpa 설정된 객체로 선언해주면 되고 가장 중요한 함수는 GrantedAuthority 함수이다. 이 함수에서 각 페이지에 관한 권한을 부여하게 된다.
결론적으로 service는 단순히 detail 객체를 콜하며, detail은 Authority(권한)을 부여만 해주는 역할만 한다. 이런것들의 전체를 비교해주는 것은 Provider에서 이루어진다.
이제 login handler로 등록한 빈을 살펴보자.
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 | package com.gno.sample.security; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException { // TODO Auto-generated method stub System.out.println(auth.getAuthorities().getClass().getName()); List<GrantedAuthority> authorities = (List<GrantedAuthority>) auth.getAuthorities(); String strAuth = authorities.get(0).getAuthority(); Cookie cookie = new Cookie("auth", strAuth); cookie.setPath("/"); // ���߿� ������Ƽ�� ���� // cookie.setDomain("test.com"); response.addCookie(cookie); if(strAuth.equals("ROLE_ADMIN")){ response.sendRedirect(request.getContextPath() + "/admin.do"); }else{ response.sendRedirect(request.getContextPath() + "/user.do"); } } } | cs |
권한에 따라 어느 페이지로 갈지 redirect 해주는거 말고 없다. failure역시 마찬가지다.
이제 interceptor를 살펴보자.
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | package com.gno.sample.security; import java.util.Collection; import java.util.Enumeration; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.log4j.Logger; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; /* * 여기서는 세션만 체크하지 authority 가지고는 아무짓도 안 합니다 */ public class CustomInterceptor extends HandlerInterceptorAdapter { // HandlerInterceptorAdapter 를 상속 받아야 intercepter 등록이 가능함 private static final Logger logger = Logger .getLogger(CustomInterceptor.class); // public String getSessionID() { // SecurityContext ctx = SecurityContextHolder.getContext(); // // if (SecurityContextHolder.getContext().getAuthentication() != null) { // WebAuthenticationDetails detail = (WebAuthenticationDetails) ctx // .getAuthentication().getDetails(); // // return detail.getSessionId(); // // } // return null; // } @SuppressWarnings("unchecked") @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // boolean result = false; String rootPath = request.getContextPath(); if(request.getServletPath().equals("/login.do")){ return true; } try { logger.debug("enter intercepter"); HttpSession session = request.getSession(false); if (session == null) { // session non exist response.sendRedirect("login.do"); // response.sendRedirect(rootPath+"/login"); // index.jsp 로 이동, web.xml 에 설정 되어 있음 (<welcome-file-list> 태그) return false; } else { SecurityContext ctx = (SecurityContext) session .getAttribute("SPRING_SECURITY_CONTEXT"); if(ctx == null){ response.sendRedirect(rootPath+"/login.do"); return false; }else{ return true; } } } catch (Exception e) { e.printStackTrace(); logger.debug(e.getMessage()); return false; } } } | cs |
interceptor는 preHandle과 postHandle이 있는데 Contoller의 함수가 호출되기전 과 호출된후의 일들을 검사(?) 할수 있다. login의 경우 페이지간 이동 포함해서 session 유지를 위한 것들을 검증하기 때문에 preHandle에 선언을 하며 SecurityContext의 session만 검사를 해주는게 끝이다. true일경우는 현재 가지고 있는 url로 이동하며 false일 경우는 redirect 곳으로 이동할때 return 값을 false로 선언한다.
여기까지 spring security의 설명은 끝이며 모든 소스는 (https://github.com/gnogun/SpringSecuritySample.git)에 있다.
Spring Security는 기존 mvc model2에서 일일히 login 관련 한 것들을 알아서 해준다. 예를들어 암호화(hash) 비교, session 체크(cookie 포함), 이중 로그인 체크등 이에 따른 예외 처리등을 관리해준다. 물론 더 많은 기능이 있겠지만, 여기서는 로그인 관련한 것들을 알아보자.
spring 4.x를 사용하였으며 derby db를 사용했으며 jpa를 사용하며 간단히 만든다.
먼저 pom 파일에 security관련 lib를 추가하자.
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 | <!-- Spring Security --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>4.0.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>4.0.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>4.0.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>4.0.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <version>4.0.3.RELEASE</version> </dependency> | cs |
그리고 web.xml에 security관련 context.xml과 security에 필요한 Filter를 추가한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring/root-context.xml, /WEB-INF/spring/security-context.xml</param-value> </context-param> .... <!--생략 ----> ... <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> | cs |
그럼 이제 context.xml을 설정해보자. 먼저 jpa관련 설정을 먼저한다. jpa 관련 설정은 root_context.xml에 설정한다.
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 | <?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:jpa="http://www.springframework.org/schema/data/jpa" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource"> <property name="driverClass" value="org.apache.derby.jdbc.ClientDriver"></property> <property name="username" value="user"></property> <property name="password" value="gnogun"></property> <property name="url" value="jdbc:derby://localhost:1527/txtest"></property> </bean> <jpa:repositories base-package="com.gno.sample.repository" entity-manager-factory-ref="entityManagerFactory"></jpa:repositories> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" /> </property> <property name="dataSource" ref="dataSource" /> <property name="jpaProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.DerbyDialect</prop> <prop key="hibernate.default_schema">txtest2</prop> <prop key="hibernate.connection.pool_size">1</prop> <prop key="hibernate.connection.shutdown">true</prop> <prop key="hibernate.show_sql">true</prop> <!-- SQL 출력 --> <prop key="hibernate.ddl_auto">auto</prop> <!-- <prop key="hibernate.hbm2ddl.auto">create</prop> --> </props> </property> <property name="packagesToScan" value="com.gno.sample.dto" /> </bean> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"></property> </bean> </beans> | cs |
jpa 설정은 찾아보기로 하고 여기에선 스킵한다.
servlet-context.xml 을 설정한다.
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 | <?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" 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"> <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure --> <!-- Enables the Spring MVC @Controller programming model --> <annotation-driven /> <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory --> <resources mapping="/resources/**" location="/resources/" /> <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory --> <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <beans:property name="prefix" value="/WEB-INF/views/" /> <beans:property name="suffix" value=".jsp" /> </beans:bean> <context:component-scan base-package="com.gno.sample" /> <interceptors> <beans:bean class="com.gno.sample.security.CustomInterceptor" /> </interceptors> </beans:beans> | cs |
interceptors 라는 tag가 있는데 controller에서 오는 값을 aop처럼 관리 하지만 security에선 session에 관한 권한 및 session을 관리한다. 이후 소스 부분에 다시 설명하겠다.
이제 security-context.xml를 보자.
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | <?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:security="http://www.springframework.org/schema/security" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.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"> <context:annotation-config></context:annotation-config> <!-- user-expressions 는 intercept-url태그의 access가 true일 경우 정의된 함수 ( hasAnyRole, isAnonymous() 등등 ) 를 사용할 수 있게 해준다. --> <security:http auto-config='true' use-expressions="true"> <!-- spring 4.x때 추가된 옵션으로 ssl을 사용하지 않을 경우 csrf는 disalbed true로 해준다. --> <security:csrf disabled="true" /> <!-- autoconfig=false 면? filter도 --> <!-- <security:intercept-url pattern="/login" access="isAnonymous()" /> --> <!-- access 이름들은 prefix가 정해져 있음 (default값 ROLE_ ) 재정의 하는 방법은 찾아놨는데 이름을 뭘 붙일지 몰라서 그냥 default prefix 사용했음 --> <security:intercept-url pattern="/admin.do" access="hasAnyRole('ROLE_ADMIN')" /> <security:intercept-url pattern="/main.do" access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')" /> <security:intercept-url pattern="/user.do" access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')" /> <security:intercept-url pattern="/*" access="permitAll" /> <!-- access="hasAnyRole('ROLE_USER', 'ROLE_ADMIN')" --> <!-- <security:anonymous /> <security:x509/> <security:http-basic /> <security:session-management></security:session-management> <security:expression-handler ref=""/> <security:custom-filter ref=""/> <security:port-mappings></security:port-mappings> <security:request-cache ref=""/> <security:remember-me/> --> <!-- always-use-default-target='true' = 서버가 죽었다 살아났을때 기존 가려고 했던 페이지를 무시하고 무조건 handler에 정의된 페이지로 이동 --> <!--authentication-failure-handler-ref와 authentication-success-handler-ref를 사용하지 않을경우는 authentication-failure-url속성을 사용하여 리다이렉트를 해준다. --> <security:form-login login-page="/login.do" default-target-url="/main.do" authentication-success-handler-ref="loginSuccessHandler" authentication-failure-handler-ref="loginFailureHandler" always-use-default-target="true" login-processing-url="/loginProcess" username-parameter="username" password-parameter="password" /> <!-- authentication-failure-url="/login" login-processing-url="" password-parameter="" username-parameter="" --> <security:logout logout-url="/logout" invalidate-session="true" success-handler-ref="logoutSuccessHandler" /> <!-- delete-cookies="JSESSIONID,auth" logout-success-url="/login.do" /> delete-cookies="" logout-url="" invalidate-session="true" success-handler-ref="" --> </security:http> <security:authentication-manager> <!-- <security:authentication-provider ref="userProvider"> </security:authentication-provider> --> <security:authentication-provider ref="CustomAuthenticationProvider"> </security:authentication-provider> </security:authentication-manager> <!-- provider는 이미 form에서 id 및 pwd(암호화 된값)을 가져오고 db에서 가져온 값을 UserService를 통해 UserDetail을 저장을 하며 UserDetail은 인증정보(db에서 가져온 사용자 값) 과 권한정보를 가져와서 provider는 먼저 인증을 비교한후 true가 되면 권한(Grant Authority)을 부여한다. --> <bean id="CustomAuthenticationProvider" class="com.gno.sample.security.CustomAuthenticationProvider"> <property name="userDetailsService" ref="userService"></property> <property name="passwordEncoder" ref="passwordEncoder"></property> </bean> <!-- UserDeatilService(com.gno.sample.security.CustomUserDetailService) 클래스는 인증(authentication)에 사용할 UserDetails 객체를 생성하는 작업이고 , UserDetails는 db에서 id값으로 user의 정보 및 권한(authority)정보를 저장한다. 이상 스러운건 이미 암호화 값으로 변경이 되있다. --> <bean id="userService" class="com.gno.sample.security.CustomUserDetailService" /> <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean> <bean id="loginSuccessHandler" class="com.gno.sample.security.LoginSuccessHandler"></bean> <bean id="loginFailureHandler" class="com.gno.sample.security.LoginFailureHandler"></bean> <bean id="logoutSuccessHandler" class="com.gno.sample.security.CustomLogoutSuccessHandler"></bean> </beans> | cs |
굉장히 많은거 같은데 한개씩 보도록 하자.
먼저 security:http 태그를 살펴보자. 속성으로 auto-config라는 넘이 있다
이넘이 true 일경우 filter는 defalut 값으로 되며 만약 false라면
<security:anonymous />
<security:x509/>
<security:http-basic />
<security:session-management></security:session-management>
<security:expression-handler ref=""/>
<security:custom-filter ref=""/>
<security:port-mappings></security:port-mappings>
<security:request-cache ref=""/>
<security:remember-me/>
위의 filter들을 셋팅해줘야 한다.
다음 속성으로 user-expressions 라는 속성은 만약 false라면 spring에서 제공하는 hasAnyRole,isAnonymous() 등 내부 함수를 사용하지 못한다. 하지만 보통은 사용하므로 true로 해준다.
csrf는 spring4.x에 추가된 태그로 ssl등을 사용하지 않을때는 disabled=true로 설정을 해준다.
다음은 interceptor-url 이다. 이 태그는 각 url별 권한( autority)를 부여해준다. 그리고 높은 권한일 경우 먼저 써주고 낮은 권한 일 경우 아래에 써주는 것을 권장 하고 있다. 위의 설정 파일을 보면 ADMIN 권한 먼저 그다음 ADMIN과 User 그다음은 permitAll 로 주고 있다. 만약 순서가 잘못되면 권한 문제로 페이지가 잘못 나올경우 있으니 주의 하자.
그리고 login과 logout은 각 handler가 존재한다. 먼저 login을 살펴보자.
속성 |
설명 |
login-page |
로그인 page |
default-target-url |
로그인 성공시 이동할 url 설정 |
authentication-success-handler-ref |
로그인 성공시에 대한 프로세스 정의 보통 권한이 많을 경우 이 핸들러에서 redirect로 설정하며, defalut-target-url은 사용하지 않는다. |
authentication-failure-handler-ref |
로그인 실패시에 대한 프로세스 정의 |
always-use-default-target |
WAS 서버가 죽었다 살아 났을때 기존 가려고 했던 페이지는 무시하고 무조건 핸들러에 정의된 페이지로 이동 |
login-processing-url |
로그인 처리에 대한 url 어떠한 controller 든지 이런것은 정의 되지 않지만 로그인 form 내에서 action url은 이 url로 정의 되야 하며 내부적으로 이 url로 로그인 processing이 진행된다. |
username-parameter, password-parameter |
만약 이 파라미터가 없다면 스프링에서 제공되는 j_username, j_password를 사용해야한다. |
logout은 invalidate-session의 경우 logout이 진행되면 session 정보를 설정값에 따라 삭제를 진행한다. true일경우 삭제.
암호화 방식은 bean으로 설정을 하며, 위의 설정 파일에는 id는 passwordEncoder이며 암호화 방식 bcrypt를 사용한다.
자 이제 마지막으로 3가지가 등장한다. AuthenticationProvider, UserService, UserDetail 이 존재한다.
provider는 이미 form에서 id 및 pwd(암호화된 값)과 db의 값을 비교한후 true이며 권한(Grant Autority)를 부여한다. 이때 비교를 하기위해 참조값으로 암호화 방식의 bean을 등록해야한다.
UserService는 인증(authentication)에 사용할 UserDetail 객체를 생성한다.
UserDeatil은 user의 정보 및 권한 정보를 저장한다.
여기까지 설정은 모두 끝났다. 다음 글에서 소스를 살펴보자.
hibernate나 jpa나 구동 방식은 매우 흡사하나. jpa는 repository를 제공한다. 기본적으로 두개다 m:n의 경우 manytomany라는 어노테이션을 사용 하지만 가끔 m:n의 테이블에 추가 컬럼을 쓸경우가 생긴다.
이때는 manytomany라는 어노테이션을 사용하지 않고 oneToMany를 사용하여 m:n 테이블에 컬럼을 추가한다. 그럼 예제를 보자.
먼저 예제는 project라는 테이블과 person 테이블이 있고 이들을 project_groups라는 테이블로 OneToMany로 맵핑하는 구조이다.
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | package com.zest.jpa.manytomanyextracolumn; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; @Entity(name="person") public class Person { private int idx; private String name; private String email; private String password; private List<Group> groups; public Person() { // TODO Auto-generated constructor stub } public Person(String name, String email, String password) { // TODO Auto-generated constructor stub this.name = name; this.email = email; this.password = password; } public Person(String name, String email, String password, List<Group> groups) { // TODO Auto-generated constructor stub this.name = name; this.email = email; this.password = password; this.groups = groups; } @Id @GeneratedValue(strategy = GenerationType.AUTO) public int getIdx() { return idx; } public void setIdx(int idx) { this.idx = idx; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @OneToMany(mappedBy = "person", cascade = CascadeType.ALL) public List<Group> getGroups() { return groups; } public void setGroups(List<Group> groups) { this.groups = groups; } @Override public String toString() { // TODO Auto-generated method stub return "name=" + name + ", password=" + password + ", email=" + email; } } | 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 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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | package com.zest.jpa.manytomanyextracolumn; import java.util.ArrayList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; @Entity(name="project") public class Project { private int idx; private String name; private String description; private List<Group> groups; public Project() { // TODO Auto-generated constructor stub } public Project(String name, String description){ this.name = name; this.description = description; this.groups = new ArrayList<Group>(); } @Id @GeneratedValue(strategy = GenerationType.AUTO) public int getIdx() { return idx; } public void setIdx(int idx) { this.idx = idx; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @OneToMany(mappedBy = "project", cascade = CascadeType.ALL, fetch=FetchType.LAZY) public List<Group> getGroups() { return groups; } public void setGroups(List<Group> groups) { this.groups = groups; } @Override public String toString() { // TODO Auto-generated method stub return "name=" + name + ", desc=" + description; } } | cs |
각 클래스는 @OnetoMany라는 어노티에션이 groups() 메소드에 묶여 있다. 그럼 이들을 묶어주는 groups 클래스를 보자.
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 62 | package com.zest.jpa.manytomanyextracolumn; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; @Entity(name="project_groups") public class Group implements Serializable { private String role; private Person person; private Project project; public Group() { // TODO Auto-generated constructor stub } public Group(Person person, Project project, String role) { this.person = person; this.project = project; this.role = role; } @Column(name = "role") public String getRole() { return role; } public void setRole(String role) { this.role = role; } @Id @ManyToOne @JoinColumn(name="person_id") public Person getPerson() { return person; } public void setPerson(Person person) { this.person = person; } @Id @ManyToOne @JoinColumn(name="project_id") public Project getProject() { return project; } public void setProject(Project project) { this.project = project; } } | cs |
각 테이블과 맵핑되는 객체에 @ManyToOne이 존재하며 @JoinColumn으로 fk를 걸어주었다. 그리고 extra column으로 role이 존재 한다.
마지막 테스크 코드를 보자
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Test public void projectMakeEx(){ Person person = new Person("gno2222222", "gnogun@naver.com", "gno"); Project project = new Project("project122222", "description"); Group group = new Group(person, project, "owner"); project.getGroups().add(group); personRepository.save(person); projectRepository.save(project); } | cs |
각 person과 project의 repository를 사용하여 save를 하고 group은 project에 add 리스트를 해주었다. 구조가 project가 추가 되면 group도 추가 되는 구조이기때문에 위 처럼 선언한것이다. 물론 반대인 경우 person이 추가될때 그럼을 add 해도 추가 된다.
소스는 https://github.com/zest133/hibernateTest.git 에 있으며 기존 hibernate 소스는 이 예제때문에 hibernate 버전이 변경 됨에 따라 소스 부분이 변경된 곳이 있을것이다.
hibernate의 HQL을 알아보자.
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 | package com.zest.hibernate.chapter10.crud; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.NamedQueries; import javax.persistence.Table; @Entity @Table(name="USER_DETAILS") public class UserDetails { @Id @GeneratedValue(strategy=GenerationType.AUTO) private int userId; private String userName; public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } } | cs |
위와 같은 테이블을 생성한다.
먼저 데이터를 insert 한다. 이 부분은 기존에 만들어 놓은 코드를 조금 수정해서 insert를 한다.
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 | @Test public void insertTest() { // hibernate의 persitst를 위한 클래스 AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(UserDetails.class); // db 접속 정보들을 저장. config.configure("hibernate.cfg.xml"); // db 접속후 여러개의 테이블을 자동으로 제너레이트 해주는 객체. // <property name="hibernate.default_schema">TESTSCHEMA</property> 이구문역시 // 마찬가지임. new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); for (int i = 0; i < 10; i++) { UserDetails user = new UserDetails(); user.setUserName("user " + i); session.save(user); } session.getTransaction().commit(); // session.close(); } | cs |
user0, user1 ........... 이런 user를 총 10개를 만들었다. 그럼 기존에 사용했던 방식으로 select의 결과물을 가져와보자.
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 | @Test public void getTest() { // hibernate의 persitst를 위한 클래스 AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(UserDetails.class); // db 접속 정보들을 저장. config.configure("hibernate.cfg.xml"); // db 접속후 여러개의 테이블을 자동으로 제너레이트 해주는 객체. // <property name="hibernate.default_schema">TESTSCHEMA</property> 이구문역시 // 마찬가지임. // new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); UserDetails user = (UserDetails) session.get(UserDetails.class, 6); System.out.println("name\t"+user.getUserName()); session.getTransaction().commit(); // session.close(); } | cs |
session.get이라는 함수를 통해 파라미터로 어느 테이블(객체), 기본키를 넣어서 값을 가져오는 예제이다.
그럼 hql를 해보자.
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 | @Test public void hsqlTest1() { // hibernate의 persitst를 위한 클래스 AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(UserDetails.class); // db 접속 정보들을 저장. config.configure("hibernate.cfg.xml"); // db 접속후 여러개의 테이블을 자동으로 제너레이트 해주는 객체. // <property name="hibernate.default_schema">TESTSCHEMA</property> 이구문역시 // 마찬가지임. // new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); //hibernate 용으로. Query query= session.createQuery("from UserDetails"); List users = query.list(); session.getTransaction().commit(); // session.close(); System.out.println("list size==>"+users.size()); } | cs |
위의 내용은 전체 리스트를 가져오는 예제이다. 이때 중요한 메소드는 createQuery라는 메소드인데 이 뒤는 sql문과 매우 흡사하다. 여기서 중요한 점은 UserDetails는 table명이 아니라 객체 라는 점이다. jpa에서는 jpa ql 이라는 것과 거의 비슷하게 맵핑이 될것이다.
한개더 예제를 해보자.
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 | @Test public void hsqlWhereTest() { // hibernate의 persitst를 위한 클래스 AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(UserDetails.class); // db 접속 정보들을 저장. config.configure("hibernate.cfg.xml"); // db 접속후 여러개의 테이블을 자동으로 제너레이트 해주는 객체. // <property name="hibernate.default_schema">TESTSCHEMA</property> 이구문역시 // 마찬가지임. // new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); //hibernate 용으로. Query query= session.createQuery("from UserDetails where userId >5"); List users = query.list(); session.getTransaction().commit(); // session.close(); System.out.println("list size==>"+users.size()); } | cs |
위의 예제는 where절이 추가 되었으며 마찬가지로 UserDetails의 멤버 변수의 값을 5보다 큰 값을 가져오는 예제이다.
이번엔 게시판 또는 그리드에서 많이 쓰는 paging에 대해 보자.
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 | @Test public void hsqlpageingTest2() { // hibernate의 persitst를 위한 클래스 AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(UserDetails.class); // db 접속 정보들을 저장. config.configure("hibernate.cfg.xml"); // db 접속후 여러개의 테이블을 자동으로 제너레이트 해주는 객체. // <property name="hibernate.default_schema">TESTSCHEMA</property> 이구문역시 // 마찬가지임. // new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); //hibernate 용으로. Query query= session.createQuery("from UserDetails "); //시작 row를 설정. query.setFirstResult(5); //결과 개수 query.setMaxResults(4); List<UserDetails> users = (List<UserDetails>)query.list(); session.getTransaction().commit(); // session.close(); for(UserDetails u : users){ System.out.println(u.getUserName()); } } | cs |
setFirstResult 메소드와 setMaxResults가 추가 되었다. 이 메소드를 이용해 paging을 쉽게 구현할 것이다.
이번엔 preparedstatement와 비슷한 binding에 대해 알아보자.
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 | @Test public void hsqlBindingTest2() { // hibernate의 persitst를 위한 클래스 AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(UserDetails.class); // db 접속 정보들을 저장. config.configure("hibernate.cfg.xml"); // db 접속후 여러개의 테이블을 자동으로 제너레이트 해주는 객체. // <property name="hibernate.default_schema">TESTSCHEMA</property> 이구문역시 // 마찬가지임. // new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); String minUserId= "5"; //hibernate 용으로. Query query= session.createQuery("from UserDetails where userId > ?"); query.setInteger(0, Integer.parseInt(minUserId)); List<UserDetails> users = (List<UserDetails>)query.list(); session.getTransaction().commit(); // session.close(); for(UserDetails u : users){ System.out.println(u.getUserName()); } } | cs |
hql에 ? 라는 바인딩 변수가 있다. 이 값을 setInteger로 바인딩 하는 메소드를 추가하고 리스트를 가져오는 예제이다. preparedstatement와 매우 흡사하다.
여기까지 하이버네이트는 끝낸다.
모든 예제는 https://github.com/zest133/hibernateTest.git 에서 있다.
다대다에 대해서 알아보자.
바로 소스 공개...
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 | package com.zest.hibernate.chapter8.manytomanymapping; import java.util.ArrayList; import java.util.List; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; @Entity public class Delegate { private int delegateId; private String delegateName; private List<Event> events = new ArrayList<Event>(); @Id @GeneratedValue public int getDelegateId() { return delegateId; } public void setDelegateId(int delegateId) { this.delegateId = delegateId; } public String getDelegateName() { return delegateName; } public void setDelegateName(String delegateName) { this.delegateName = delegateName; } // 결과를 담을 테이블명을 정의 . @ManyToMany @JoinTable(name = "JOIN_DELEGATE_EVENT", joinColumns = { @JoinColumn(name = "delegateId") }, inverseJoinColumns = { @JoinColumn(name = "eventId") } ) public List<Event> getEvents() { return events; } public void setEvents(List<Event> events) { this.events = events; } } | 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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | package com.zest.hibernate.chapter8.manytomanymapping; import java.util.ArrayList; import java.util.List; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; @Entity public class Event { private int eventId; private String eventName; private List<Delegate> delegates = new ArrayList<Delegate>(); @Id @GeneratedValue public int getEventId() { return eventId; } public void setEventId(int eventId) { this.eventId = eventId; } public String getEventName() { return eventName; } public void setEventName(String eventName) { this.eventName = eventName; } @ManyToMany @JoinTable(name = "JOIN_DELEGATE_EVENT", joinColumns = { @JoinColumn(name = "eventId") }, inverseJoinColumns = { @JoinColumn(name = "delegateId") }) public List<Delegate> getDelegates() { return delegates; } public void setDelegates(List<Delegate> delegates) { this.delegates = delegates; } } | cs |
두개의 객체를 선언하였다. 여기도 마찬가지로 새로운 어노테이션이 등장한다.
@ManyToMany 이다. 그리고 JoinColumn이 아닌 JoinTable이다.
@ManyToMany는 두개의 객체 두군데 다 선언을 한다. 그리고 필연적으로 @JoinTable을 사용해야한다. 각 속성을 알아보자. name은 m:n에 대한 데이터를 저장하는 테이블 이며, jonColumns와 inverseJoinColumkns가 존재한다. joinColumns는 join이 되는 컬럼 여기선 상호간의 컬럼 아이디를 써준다.
결과 쿼리를 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Hibernate: insert into TESTSCHEMA.Delegate (delegateName, delegateId) values (?, ?) Hibernate: insert into TESTSCHEMA.Delegate (delegateName, delegateId) values (?, ?) Hibernate: insert into TESTSCHEMA.Delegate (delegateName, delegateId) values (?, ?) Hibernate: insert into TESTSCHEMA.Delegate (delegateName, delegateId) values (?, ?) Hibernate: insert into TESTSCHEMA.Event (eventName, eventId) values (?, ?) Hibernate: insert into TESTSCHEMA.Event (eventName, eventId) values (?, ?) Hibernate: insert into TESTSCHEMA.Event (eventName, eventId) values (?, ?) Hibernate: insert into TESTSCHEMA.JOIN_DELEGATE_EVENT (eventId, delegateId) values (?, ?) Hibernate: insert into TESTSCHEMA.JOIN_DELEGATE_EVENT (eventId, delegateId) values (?, ?) Hibernate: insert into TESTSCHEMA.JOIN_DELEGATE_EVENT (eventId, delegateId) values (?, ?) Hibernate: insert into TESTSCHEMA.JOIN_DELEGATE_EVENT (eventId, delegateId) values (?, ?) Hibernate: insert into TESTSCHEMA.JOIN_DELEGATE_EVENT (eventId, delegateId) values (?, ?) Hibernate: insert into TESTSCHEMA.JOIN_DELEGATE_EVENT (eventId, delegateId) values (?, ?) | cs |
JOIN_DELEGATE_EVENT 테이블 생성되며 각 각의 정보가 m:n으로 맵핑이 된다.
하이버네이트 모든 코드는 https://github.com/zest133/hibernateTest.git 여기에 저장되어 있다.
이번엔 1:N 관계를 살펴보자.
바로 소스를 보자.
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 | package com.zest.hibernate.chapter7.onetomanymapping; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToMany; @Entity public class College { @Id @GeneratedValue private int collegeId; private String collegeName; @OneToMany(targetEntity=Student.class, mappedBy="college" ,cascade= CascadeType.ALL,fetch=FetchType.EAGER) private List<Student> students ; public int getCollegeId() { return collegeId; } public void setCollegeId(int collegeId) { this.collegeId = collegeId; } public String getCollegeName() { return collegeName; } public void setCollegeName(String collegeName) { this.collegeName = collegeName; } public List<Student> getStudents() { return students; } public void setStudents(List<Student> students) { this.students = students; } } | 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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | package com.zest.hibernate.chapter7.onetomanymapping; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; @Entity public class Student { @Id @GeneratedValue private int studentId; private String studentName; @ManyToOne @JoinColumn(name="college_id") private College college; public int getStudentId() { return studentId; } public void setStudentId(int studentId) { this.studentId = studentId; } public String getStudentName() { return studentName; } public void setStudentName(String studentName) { this.studentName = studentName; } public College getCollege() { return college; } public void setCollege(College college) { this.college = college; } } | cs |
몇가지 새로운 어노테이션이 등장했다. @OneToMany, @ManyToOne
OneToMany가 선언되있는 클래스는 One을 지칭하고 targetEntity는 many가 된다.
mappedBy는 One에 속하는 테이블 명을 써주면 된다.
Student 객체는 ManyToOne을 선언하고 JoinColumn의 컬럼 명을 정의해주면 된다.
그럼 테스트 ㄱㄱ
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 | package com.zest.hibernate.chapter7.onetomanymapping; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.AnnotationConfiguration; import org.hibernate.tool.hbm2ddl.SchemaExport; import org.junit.Test; import com.zest.hibernate.chapter5.inheritancemapping.Module; import com.zest.hibernate.chapter5.inheritancemapping.Project; import com.zest.hibernate.chapter5.inheritancemapping.Task; public class OneToManyTest { @Test public void oneTest(){ AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(College.class); config.addAnnotatedClass(Student.class); config.configure("hibernate.cfg.xml"); new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); College college1 = new College(); college1.setCollegeName("Newyork College"); Student s1 = new Student(); s1.setStudentName("Alex Rod"); Student s2 = new Student(); s2.setStudentName("Linda Berry"); s1.setCollege(college1); s2.setCollege(college1); session.save(college1); session.save(s1); session.save(s2); //session.save(alexdetail); 이건 안함. 카스케이드 라서 session.getTransaction().commit(); } } | cs |
결과는 다음과 같다.
1 2 3 4 | Hibernate: insert into TESTSCHEMA.College (collegeName, collegeId) values (?, ?) Hibernate: insert into TESTSCHEMA.Student (college_id, studentName, studentId) values (?, ?, ?) Hibernate: insert into TESTSCHEMA.Student (college_id, studentName, studentId) values (?, ?, ?) | cs |
예상했던 대로 College_ID는 FK로 셋팅이 된다.
이번엔 join에 대해 알아보자. join은 1:1 맵핑, 1:m, m:n 아마도 이렇게 구성이 될것이다.
여기서는 1:1에 대해 알아보자.
객체는 person과 personDetail 두가지가 있다. 그럼 코드를 보자.
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | package com.zest.hibernate.chapter6.onetoonemapping; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.OneToOne; /** * * one to one * ex) person data table * person id : 1 *person name: aaaa *pdetail_fk :32768 이넘 * *person_detail *detailId_pk : 32768 이넘 *income:123456786 *job : adklja *zipcode : 20815 * *위에 '이넘' 두개가 1:1로 맵핑. */ @Entity public class Person { @Id @GeneratedValue private int personId; private String personName; /** * CascadeType의 종류에는 다음과 같은 것들이 있다. CascadeType.RESIST – 엔티티를 생성하고, 연관 엔티티를 추가하였을 때 persist() 를 수행하면 연관 엔티티도 함께 persist()가 수행된다. 만약 연관 엔티티가 DB에 등록된 키값을 가지고 있다면 detached entity passed to persist Exception이 발생한다. CascadeType.MERGE – 트랜잭션이 종료되고 detach 상태에서 연관 엔티티를 추가하거나 변경된 이후에 부모 엔티티가 merge()를 수행하게 되면 변경사항이 적용된다.(연관 엔티티의 추가 및 수정 모두 반영됨) CascadeType.REMOVE – 삭제 시 연관된 엔티티도 같이 삭제됨 CascadeType.DETACH – 부모 엔티티가 detach()를 수행하게 되면, 연관된 엔티티도 detach() 상태가 되어 변경사항이 반영되지 않는다. CascadeType.ALL – 모든 Cascade 적용 fetch 의 순위 두 개 있다 fetch=FetchType.EAGER: 실행 문 바로 꺼냈다 fetch=FetchType.LAZY: 쓸 때 비로소 꺼냈다 */ @OneToOne(cascade=CascadeType.ALL, fetch= FetchType.EAGER) @JoinColumn(name="pDetail_FK") private PersonDetail pDetail; public PersonDetail getpDetail() { return pDetail; } public void setpDetail(PersonDetail pDetail) { this.pDetail = pDetail; } public int getPersonId() { return personId; } public void setPersonId(int personId) { this.personId = personId; } public String getPersonName() { return personName; } public void setPersonName(String personName) { this.personName = personName; } } | cs |
새로운 어노테이션이 등장한다. OneToOne 과 JoinColumn 이 추가 되었다.
OneToOne의 경우 외래키를 만들때 선언하는 어노테이션이다. Cascade속성과 Fetch 속성 두가지를 지원한다. JoinColumn은 외래키가 되는 Column의 실제 Column이름을 지정하는것이다. 그냥 테이블 생성시에는 @Column이라는 놈이랑 비슷하다고 생각하면된다. 그리고 중요한 것은 기본키가 되는 객체에 어노테이션을 선언한다는것이다.
그럼 personDetail을 만들어보자.
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 | package com.zest.hibernate.chapter6.onetoonemapping; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class PersonDetail { @Id @GeneratedValue @Column(name="detailId_PK") private int personDetailId; private String zipCode; private String job; private double income; public int getPersonDetailId() { return personDetailId; } public void setPersonDetailId(int personDetailId) { this.personDetailId = personDetailId; } public String getZipCode() { return zipCode; } public void setZipCode(String zipCode) { this.zipCode = zipCode; } public String getJob() { return job; } public void setJob(String job) { this.job = job; } public double getIncome() { return income; } public void setIncome(double income) { this.income = income; } } | 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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package com.zest.hibernate.chapter6.onetoonemapping; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.AnnotationConfiguration; import org.hibernate.tool.hbm2ddl.SchemaExport; import org.junit.Test; import com.zest.hibernate.chapter5.inheritancemapping.Module; import com.zest.hibernate.chapter5.inheritancemapping.Project; import com.zest.hibernate.chapter5.inheritancemapping.Task; public class OneToOneTest { @Test public void oneTest(){ AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(Person.class); config.addAnnotatedClass(PersonDetail.class); config.configure("hibernate.cfg.xml"); new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); PersonDetail alexDetail = new PersonDetail(); alexDetail.setZipCode("20815"); alexDetail.setJob("Accountant"); alexDetail.setIncome(67245.56); Person alex = new Person(); alex.setPersonName("alex berry"); alex.setpDetail(alexDetail); session.save(alex); //session.save(alexdetail); 이건 안함. 카스케이드 라서 session.getTransaction().commit(); } } | cs |
결과는 다음과 같다.
1 2 3 | Hibernate: insert into TESTSCHEMA.PersonDetail (income, job, zipCode, detailId_PK) values (?, ?, ?, ?) Hibernate: insert into TESTSCHEMA.Person (pDetail_FK, personName, personId) values (?, ?, ?) | cs |
테이블 구조를 보자. Person의 테이블을 살펴보면 PersonId는 기본키이며 pDetail_FK는 외래키로 생성이 될것이다. 여기까지 OneAndOne 설명을 마친다.
이번엔 상속 관계에 대해 알아보자.
Project, Module, Task 이 3개 객체가 있다.
최상위 객체는 Project이며, 그 자식으로 Module 이 있으며, Module의 자식으로 Task가 있다.
먼저 소스를 보자.
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 | package com.zest.hibernate.chapter5.inheritancemapping; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Inheritance; import javax.persistence.InheritanceType; @Entity //single table 실행시 stategy가 없는것과 동일한 결과를 볼수있다. @Inheritance(strategy=InheritanceType.SINGLE_TABLE) //각 class마다 table이 생성되며, 하위 class들은 pk키를 기준으로 테이블이 생성된다. //또, 최상위 클래스는 pk와 하위 컬럼만 존재한다. //@Inheritance(strategy=InheritanceType.JOINED) //class에 맞는 컬럼 값만 저장하며, pk는 최상위 클래스 값으로 중복되지 않은 값으로 저장된다. //@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS) public class Project { @Id @GeneratedValue private int projectId; private String projectName; public int getProjectId() { return projectId; } public void setProjectId(int projectId) { this.projectId = projectId; } public String getProjectName() { return projectName; } public void setProjectName(String projectName) { this.projectName = projectName; } } | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package com.zest.hibernate.chapter5.inheritancemapping; import javax.persistence.Entity; @Entity public class Module extends Project{ private String moduleName; public String getModuleName() { return moduleName; } public void setModuleName(String moduleName) { this.moduleName = moduleName; } } | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package com.zest.hibernate.chapter5.inheritancemapping; import javax.persistence.Entity; @Entity public class Task extends Module{ private String taskName; public String getTaskName() { return taskName; } public void setTaskName(String taskName) { this.taskName = taskName; } } | cs |
먼저 상속에 관한 옵션은 3가지 어노테이션이 존재한다.
@Inheritance(strategy=InheritanceType.SINGLE_TABLE) - 디폴트 선언이며, 하위 자식의 값들은 무시되며, 최상위 클래스의 테이블에만 정보를 넣는다.
@Inheritance(strategy=InheritanceType.JOINED) - 이름에서 알듯이 자식들은 부모와 fk로 엮이며, 부모는 자식의 값을 가지고 있고, 자식은 자신의 값을 가지고 있는다. 결국 부모는 전체의 데이터를 가지고 있고 자식은 fk로 본인의 값들을 가지고 있는다.
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS) - 부모 자식 포함해서 중복되지 않는 값을 서로간에 가지고 있는다.
아마도 제일 많이 쓰이는건 2번이 아닐까 싶다. 그럼 테스트 코드를 보자. 먼저 @Inheritance(strategy=InheritanceType.SINGLE_TABLE) 일 경우
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 | package com.zest.hibernate.chapter5.inheritancemapping; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.AnnotationConfiguration; import org.hibernate.tool.hbm2ddl.SchemaExport; import org.junit.Test; public class InheritaceMappingTest { @Test public void defaultRunTableTest(){ AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(Project.class); config.addAnnotatedClass(Module.class); config.addAnnotatedClass(Task.class); config.configure("hibernate.cfg.xml"); new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); Project p = new Project(); p.setProjectName("hibernate lessons"); Module m = new Module(); m.setProjectName("spring lessons"); m.setModuleName("aop"); Task t = new Task(); t.setProjectName("java lessons"); t.setModuleName("collections"); t.setTaskName("arraylist"); session.save(p); session.save(m); session.save(t); session.getTransaction().commit(); } } | cs |
1 2 3 4 | Hibernate: insert into TESTSCHEMA.Project (projectName, DTYPE, projectId) values (?, 'Project', ?) Hibernate: insert into TESTSCHEMA.Project (projectName, moduleName, DTYPE, projectId) values (?, ?, 'Module', ?) Hibernate: insert into TESTSCHEMA.Project (projectName, moduleName, taskName, DTYPE, projectId) values (?, ?, ?, 'Task', ?) | cs |
위의 형태로 로그가 나올 것이다. 결과를 보면 project 말고는 다른 테이블은 생성이 안될 것이다.
1 2 3 4 | drop table testschema.task; drop table testschema.module; drop table testschema.project; | cs |
다시 테이블을 드랍 시키고 @Inheritance(strategy=InheritanceType.JOINED) 이 어노테이션을 적용해보자.
1 2 3 4 5 6 7 | Hibernate: insert into TESTSCHEMA.Project (projectName, projectId) values (?, ?) Hibernate: insert into TESTSCHEMA.Project (projectName, projectId) values (?, ?) Hibernate: insert into TESTSCHEMA.Module (moduleName, projectId) values (?, ?) Hibernate: insert into TESTSCHEMA.Project (projectName, projectId) values (?, ?) Hibernate: insert into TESTSCHEMA.Module (moduleName, projectId) values (?, ?) Hibernate: insert into TESTSCHEMA.Task (taskName, projectId) values (?, ?) | cs |
Project에는 결과가 총 3개가 생성이 된다. Module에 두개의 row가 생성되며, 마지막 task에는 1개의 row가 생성 될것이다. 그리고 테이블의 구조를 살펴보면 module.projectId , task.projectId는 pk,fk로 되어있다.
마지막 @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)를 해보자. 먼저 테이블을 drop후 위에 선언한 project 객체의 주석을 변경한다. 그리고 테스트 ㄱㄱㄱㄱ
1 2 3 | Hibernate: insert into TESTSCHEMA.Project (projectName, projectId) values (?, ?) Hibernate: insert into TESTSCHEMA.Module (projectName, moduleName, projectId) values (?, ?, ?) Hibernate: insert into TESTSCHEMA.Task (projectName, moduleName, taskName, projectId) values (?, ?, ?, ?) | cs |
자신의 데이터만 row에 생길 것이다.
소스는 https://github.com/zest133/hibernateTest.git 여기에
이번엔 복합키에 대해 알아보자.
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 | package com.zest.hibernate.chapter4; import javax.persistence.Entity; import javax.persistence.Id; @Entity public class Accounts { private int userId; private int accountId; // @Id // //만약 변수에 annotaion을 할꺼면 private는 안된다. 다른 객체이므로. getxxx 에 annotaion을 붙이면 된다. // CompoundKey compoundKey; private int accountBalance; // public CompoundKey getCompoundKey() { // return compoundKey; // } // public void setCompoundKey(CompoundKey compoundKey) { // this.compoundKey = compoundKey; // } public int getAccountBalance() { return accountBalance; } public void setAccountBalance(int accountBalance) { this.accountBalance = accountBalance; } } | cs |
userId와 accountId에 복합키를 걸면 어떻게 할까?
자 새로운 객체를 한개더 생성하자. 클래스 이름은 CompundKey이다.
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 | package com.zest.hibernate.chapter4; import java.io.Serializable; import javax.persistence.Embeddable; //필히 Serializable 해줘야함. @Embeddable public class CompoundKey implements Serializable{ private int userId; private int accountId; public CompoundKey(int userId, int accountId){ this.accountId = accountId; this.userId = userId; } public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public int getAccountId() { return accountId; } public void setAccountId(int accountId) { this.accountId = accountId; } } | cs |
여기서도 @Embeddable 이라는 어노테이션을 사용한다. 중요한 점은 필히
Serializable을 해야하는 점이다. 그리고 Accounts 클래스를 수정하자.
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 | package com.zest.hibernate.chapter4; import javax.persistence.Entity; import javax.persistence.Id; @Entity public class Accounts { //compound key로 옮겼으니 주석으로 막는다. // private int userId; // private int accountId; @Id //만약 변수에 annotaion을 할꺼면 private는 안된다. 다른 객체이므로. getxxx 에 annotaion을 붙이면 된다. CompoundKey compoundKey; private int accountBalance; public CompoundKey getCompoundKey() { return compoundKey; } public void setCompoundKey(CompoundKey compoundKey) { this.compoundKey = compoundKey; } public int getAccountBalance() { return accountBalance; } public void setAccountBalance(int accountBalance) { this.accountBalance = accountBalance; } } | cs |
CompoundKey라는 객체에 @Id 붙여 기본키를 만들었다. 다른 예제와 틀리게 private가 없을 것이다. 외부에 있는 변수를 끌어와서 private를 쓰면 에러가 난다. 이때 getxxx의 함수에 @Id를 붙이거나 또는 private 이상의 권한을 줘야만 된다.
그럼 테스트 코드로...
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 | package com.zest.hibernate.chapter4; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.AnnotationConfiguration; import org.hibernate.tool.hbm2ddl.SchemaExport; import org.junit.Test; public class AccountTest { @Test public void account1() { AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(Accounts.class); config.configure("hibernate.cfg.xml"); new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); CompoundKey key1 = new CompoundKey(100, 10001); Accounts savings = new Accounts(); savings.setCompoundKey(key1); savings.setAccountBalance(8500); CompoundKey key2 = new CompoundKey(100, 20001); Accounts checking = new Accounts(); checking.setCompoundKey(key2); checking.setAccountBalance(2500); session.save(savings); session.save(checking); session.getTransaction().commit(); } } | cs |
index를 확인하면 userid와 accountid가 복합키로 생성되있을 것이다.
이번엔 두개의 클래스를 하나의 테이블로 만드는 법을 살펴 보자.
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 | package com.zest.hibernate.chapter3; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.SecondaryTable; import javax.persistence.Table; @Entity public class School { @Id @GeneratedValue private int schoolId; private String schoolName; @Embedded private SchoolDetail schoolDetail; public SchoolDetail getSchoolDetail() { return schoolDetail; } public void setSchoolDetail(SchoolDetail schoolDetail) { this.schoolDetail = schoolDetail; } public int getSchoolId() { return schoolId; } public void setSchoolId(int schoolId) { this.schoolId = schoolId; } public String getSchoolName() { return schoolName; } public void setSchoolName(String schoolName) { this.schoolName = schoolName; } } | 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 29 30 31 32 33 34 | package com.zest.hibernate.chapter3; import javax.persistence.Embeddable; @Embeddable public class SchoolDetail { private String schoolAddress; private boolean isPublishSchool; private int studentCount; public String getSchoolAddress() { return schoolAddress; } public void setSchoolAddress(String schoolAddress) { this.schoolAddress = schoolAddress; } public boolean isPublishSchool() { return isPublishSchool; } public void setPublishSchool(boolean isPublishSchool) { this.isPublishSchool = isPublishSchool; } public int getStudentCount() { return studentCount; } public void setStudentCount(int studentCount) { this.studentCount = studentCount; } } | cs |
두개의 클래스가 있는데 새로운 어노테이션 발견....
부모가 되는 클래스는에는 @Embeded 라는 어노테이션을 쓰고 자식이 되는 클래스에는 @Embedable 어노테이션을 사용한다.
나머진 기존과 다를게 없다. 그럼 테스트코드. ㄱㄱ
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 | package com.zest.hibernate.chapter3; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.AnnotationConfiguration; import org.hibernate.tool.hbm2ddl.SchemaExport; import org.junit.Test; import com.zest.hibernate.chapter2.Customer; public class SchoolTest { @Test public void school1() { AnnotationConfiguration config = new AnnotationConfiguration(); config.addAnnotatedClass(School.class); config.configure("hibernate.cfg.xml"); new SchemaExport(config).create(true, true); // 아래 두개의 구문은 객체를 트랜잭션을 컨트롤. SessionFactory factory = config.buildSessionFactory(); Session session = factory.getCurrentSession(); session.beginTransaction(); SchoolDetail annsDetail= new SchoolDetail(); annsDetail.setPublishSchool(false); annsDetail.setSchoolAddress("101 washington"); annsDetail.setStudentCount(300); School stanns = new School(); stanns.setSchoolName("st. anns school"); stanns.setSchoolDetail(annsDetail); session.save(stanns); session.getTransaction().commit(); } } | cs |
테스트 코드는 두개의 객체를 사용하여 트랜잰션에 묶어준다. 그럼 결과 sql문을 보자.
1 2 | Hibernate: insert into TESTSCHEMA.School (isPublishSchool, schoolAddress, studentCount, schoolName, schoolId) values (?, ?, ?, ?, ?) | cs |
이런 문구가 나올것이다.