tbl_member: 유저의 기본 정보 (아이디, 비번, 이름 등)tbl_member_auth: 유저의 권한(ROLE_*) 정보MemberVO: 회원 기본 정보 + List<AuthVO>로 권한을 포함AuthVO: 권한 문자열 (ROLE_USER, ROLE_ADMIN 등)CustomUser 객체 생성Spring Security에서 사용하는 UserDetails 타입의 구현체MemberVO를 내부에 들고 있어서, 이후 JSP나 서비스 로직에서 꺼내 사용 가능CustomUserDetailsService 구현 (UserDetailsService 인터페이스)userid로 사용자 조회 (MemberMapper.read)MemberVO → CustomUser로 감싸 리턴security-context.xml에서 Security 설정UserDetailsService로 customUserDetailService 사용하도록 설정<sec:authentication property="principal.member.userName"/> 등으로 유저 정보 출력 가능UserDetails 와 UserDetailsServiceUserDetailsService → loadUserByUsername() 구현UserDetails → 유저 정보를 담고 있는 객체 (기본 User, 커스터마이징 가능)CustomUserpublic class CustomUser extends User {
private MemberVO member; // 도메인 유저 객체 포함
}
User의 생성자에 username, password, authorities를 전달MemberVO를 내부에 포함시켜, JSP에서 사용자 상세 정보를 사용 가능BCryptPasswordEncoderMapper + MyBatis resultMapMemberMapper.xml에서 SQL로 사용자 + 권한 정보 조회resultMap으로 MemberVO 객체에 매핑collection 태그로 권한 리스트를 authList에 자동 매핑| tbl_member | tbl_member_auth |
|---|---|
| userid (PK) | userid (FK) |
| userpw | auth (ex. ROLE_USER) |
| username | |
| enabled | |
| regdate, updatedate |
➡️ 1:N 관계 (한 명의 유저가 여러 권한 가질 수 있음)
testInsertMember() : 더미 사용자 100명 생성testInsertAuth() : 각 사용자에 권한 부여UserDetailsService.loadUserByUsername() 호출MemberMapper.read)CustomUser로 감싸 UserDetails 리턴CustomLoginSuccessHandler 동작<sec:authentication property="..."/>로 유저 정보 사용 가능username, password 외에도 사용자 이름, 가입일 등 도메인 정보가 필요함CustomUser와 MemberVO로 Security 영역과 비즈니스 영역의 연결고리를 만드는 것이 핵심필요하다면 CustomUserDetailsService 클래스 코드도 같이 정리해줄 수 있어요!
create table tbl_member(
userid varchar2(50) not null primary key,
userpw varchar2(100) not null,
username varchar2(100) not null,
regdate date default sysdate,
updatedate date default sysdate,
enabled char(1) default '1');
create table tbl_member_auth (
userid varchar2(50) not null,
auth varchar2(50) not null,
constraint fk_member_auth foreign key(userid) references tbl_member(userid)
);
<bean id="customAccessDenied"
class="org.zerock.security.CustomAccessDeniedHandler"></bean>
<bean id = "customLoginSuccess"
class="org.zerock.security.CustomLoginSuccessHandler"></bean>
<bean id="bcryptPasswordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
<bean id="customUserDetailService"
class="org.zerock.security.CustomUserDetailService"></bean>
<security:http>
<security:intercept-url pattern="/sample3/all" access="permitAll"/>
<security:intercept-url pattern="/sample3/member"
access="hasRole('ROLE_MEMBER')"/>
<security:intercept-url pattern="/sample3/admin"
access="hasRole('ROLE_ADMIN')"/>
<security:form-login login-page="/customLogin"
authentication-success-handler-ref="customLoginSuccess"/>
<security:logout logout-url="/customLogout" invalidate-session="true"/>
<!-- <security:access-denied-handler error-page="/accessError"/> -->
<security:access-denied-handler ref="customAccessDenied"/>
</security:http>
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailService">
<security:password-encoder ref="bcryptPasswordEncoder"/>
<!-- <security:user name="member" password="{noop}member" authorities="ROLE_MEMBER"/>
<security:user name="admin" password="{noop}admin"
authorities="ROLE_MEMBER, ROLE_ADMIN"/> -->
</security:authentication-provider>
</security:authentication-manager>
...
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({
"file:src/main/webapp/WEB-INF/spring/root-context.xml",
"file:src/main/webapp/WEB-INF/spring/security-context.xml"
})
@Log4j
public class MemberTests {
@Setter(onMethod_ = @Autowired)
private PasswordEncoder pwencoder;
@Setter(onMethod_ = @Autowired)
private DataSource ds;
@Test
public void testInsertMember() {
String sql = "insert into tbl_member(userid, userpw, username) values (?,?,?)";
for(int i = 0; i < 100; i++) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = ds.getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(2, pwencoder.encode("pw" + i));
if(i <80) {
pstmt.setString(1, "user"+i);
pstmt.setString(3,"일반사용자"+i);
}else if (i <90) {
pstmt.setString(1, "manager"+i);
pstmt.setString(3,"운영자"+i);
}else {
pstmt.setString(1, "admin"+i);
pstmt.setString(3,"관리자"+i);
}
pstmt.executeUpdate();
}catch(Exception e) {
e.printStackTrace();
}finally {
if(pstmt != null) { try { pstmt.close(); } catch(Exception e) {} }
if(con != null) { try { con.close(); } catch(Exception e) {} }
}
}//end for
}
@Test
public void testInsertAuth() {
String sql = "insert into tbl_member_auth (userid, auth) values (?,?)";
for(int i = 0; i < 100; i++) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = ds.getConnection();
pstmt = con.prepareStatement(sql);
if(i <80) {
pstmt.setString(1, "user"+i);
pstmt.setString(2,"ROLE_USER");
}else if (i <90) {
pstmt.setString(1, "manager"+i);
pstmt.setString(2,"ROLE_MEMBER");
}else {
pstmt.setString(1, "admin"+i);
pstmt.setString(2,"ROLE_ADMIN");
}
pstmt.executeUpdate();
}catch(Exception e) {
e.printStackTrace();
}finally {
if(pstmt != null) { try { pstmt.close(); } catch(Exception e) {} }
if(con != null) { try { con.close(); } catch(Exception e) {} }
}
}//end for
}
}

@Data
public class MemberVO {
private String userid;
private String userpw;
private String userName;
private boolean enabled;
private Date regDate;
private Date updateDate;
private List<AuthVO> authList;
}
@Data
public class AuthVO {
private String userid;
private String auth;
}
...
@Getter
public class CustomUser extends User {
private static final long serialVersionUID = 1L;
private MemberVO member;
public CustomUser(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomUser(MemberVO vo) {
super(vo.getUserid(), vo.getUserpw(), vo.getAuthList().stream()
.map(auth -> new SimpleGrantedAuthority(auth.getAuth())).collect(Collectors.toList()));
this.member = vo;
}
}
✅ 왜 이렇게 설계했을까?
Spring Security에서 로그인한 사용자 정보를 저장할 때 UserDetails 타입을 사용하는데, User는 그 구현체입니다.
하지만 실제 서비스에서는 아이디, 비밀번호 외에도 추가적인 사용자 정보가 필요할 수 있기 때문에, 그 정보를 member 객체에 담아 함께 관리하는 방식입니다.
즉, CustomUser는 Spring Security 인증 객체와 실제 비즈니스 도메인 객체(MemberVO)를 연결해주는 역할을 합니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>/sample/admin page</h1>
<p>principal : <sec:authentication property="principal"/></p>
<p>MemberVO : <sec:authentication property="principal.member"/></p>
<p>사용자이름 : <sec:authentication property="principal.member.userName"/></p>
<p>사용자아이디 : <sec:authentication property="principal.username"/></p>
<p>사용자 권한 리스트 : <sec:authentication property="principal.member.authList"/></p>
<a href="/customLogout">Logout</a>
</body>
</html>

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.zerock.mapper.MemberMapper">
<resultMap type="org.zerock.domain.MemberVO" id="memberMap">
<id property="userid" column="userid"/>
<result property="userid" column="userid"/>
<result property="userpw" column="userpw"/>
<result property="userName" column="username"/>
<result property="regDate" column="regdate"/>
<result property="updateDate" column="updatedate"/>
<collection property="authList" resultMap="authMap">
</collection>
</resultMap>
<resultMap type="org.zerock.domain.AuthVO" id="authMap">
<result property="userid" column="userid"/>
<result property="auth" column="auth"/>
</resultMap>
<select id="read" resultMap="memberMap">
SELECT
mem.userid, userpw, username, enabled, regdate, updatedate, auth
FROM
tbl_member mem LEFT OUTER JOIN tbl_member_auth auth
on mem.userid = auth.userid
WHERE mem.userid = #{userid}
</select>
</mapper>
package org.zerock.mapper;
import org.zerock.domain.MemberVO;
public interface MemberMapper {
public MemberVO read(String userid);
}