๐ง๐ป Admin ๊ด๋ฆฌ์ ๊ด๋ฆฌ ๊ธฐ๋ฅ(Read/Write) ๊ตฌํ ํ๊ณ (๊ฒ์/์ ๋ ฌ/ํํฐ/ํ์ด์ง + ์์ /์ญํ /์ํ/์ญ์ )
์ด๋ฒ ํ ํ๋ก์ ํธ์์ ๋ด๊ฐ ๋งก์ ํํธ๋ ๊ด๋ฆฌ์(Admin) ๋๋ฉ์ธ์ด์๋ค.
์ด๋ฐ์๋ ์ธ์ฆ(์ธ์
) ๊ธฐ๋ฅ์ ๋จผ์ ๊ตฌํํ๊ณ , ๊ทธ ๋ค์ ๋จ๊ณ๋ก “๊ด๋ฆฌ์ ๊ด๋ฆฌ ๊ธฐ๋ฅ” ์ฒดํฌ๋ฆฌ์คํธ(READ/WRITE)๋ฅผ ๋๊น์ง ์์ฑํ๋ ๊ฒ ๋ชฉํ์๋ค.
โ ๋ด๊ฐ ํด์ผ ํ๋ ๊ด๋ฆฌ์ ์ฒดํฌ๋ฆฌ์คํธ
๊ด๋ฆฌ์ ์ฒดํฌ๋ฆฌ์คํธ์์ “Read/Write”๋ ์๋ 6๊ฐ์๋ค.
๐ READ
- ๊ด๋ฆฌ์ ๋ชฉ๋ก ์กฐํ(๊ฒ์/์ ๋ ฌ/ํํฐ/ํ์ด์ง)
- ๊ด๋ฆฌ์ ์์ธ ์กฐํ
๐ WRITE
- ๊ด๋ฆฌ์ ์ ๋ณด ์์ (์ด๋ฆ/์ด๋ฉ์ผ/์ ํ)
- ๊ด๋ฆฌ์ ์ญํ ๋ณ๊ฒฝ
- ๊ด๋ฆฌ์ ์ํ ๋ณ๊ฒฝ
- ๊ด๋ฆฌ์ ์ญ์ (Soft Delete๋ก ์ ํ)
๊ทธ๋ฆฌ๊ณ ์ด ๊ธฐ๋ฅ๋ค์ ์น์ธ/๊ฑฐ๋ถ(approve/reject), ๋ด ์ ๋ณด ์กฐํ/์์ , ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ๊ฐ์ ๊ธฐ์กด ๊ธฐ๋ฅ๊ณผ๋ ์์ฐ์ค๋ฝ๊ฒ ์ฐ๊ฒฐ๋๋ค.
๐งฉ ๋ด๊ฐ ๋ง๋ ๊ตฌ์กฐ(ํจํค์ง/์ญํ )
ํ ์ปจ๋ฒค์ ์ ์ต๋ํ ์ ๊นจ๊ธฐ ์ํด, admin ๋๋ฉ์ธ์ ์๋์ฒ๋ผ ์ ๋ฆฌํ๋ค.
admin
โฃ controller
โฃ dto
โ โฃ request
โ โฃ response
โ โ session
โฃ entity
โฃ repository
โ service
โฃ AdminQueryService (์กฐํ ์ ์ฉ)
โ AdminCommandService (๋ณ๊ฒฝ ์ ์ฉ)
์ Query / Command๋ฅผ ๋๋ด์๊น?
์ด๊ฑด “CQRS ๋ง๋ณด๊ธฐ” ๋ฐฉ์์ด๋ค.
- ์กฐํ(Read)๋ ๊ฒ์ ์กฐ๊ฑด/์ ๋ ฌ/ํ์ด์ง** ๊ฐ์ “์ฟผ๋ฆฌ ์ต์ ํ” ์ค์ฌ
- ๋ณ๊ฒฝ(Write)๋ ๋น์ฆ๋์ค ๊ท์น/์ํ ๋ณ๊ฒฝ/๊ฒ์ฆ** ์ค์ฌ
๊ฐ์ ์๋น์ค์ ๋ฃ์ผ๋ฉด ์ฝ๋๊ฐ ์์ฌ์ ์ปค์ง๊ณ , ํ ํ๋ก์ ํธ์์๋ ์ถฉ๋๋ ๋์ด๋๋ค.
๊ทธ๋์ ์์ ์กฐํ/์์ ํ๋ฆ์ ๋ถ๋ฆฌํ๋ค.
READ ๊ตฌํ: ๊ด๋ฆฌ์ ๋ชฉ๋ก ์กฐํ + ์์ธ ์กฐํ
READ๋ “๊ด๋ฆฌ์ ๋ชฉ๋ก ํ๋ฉด”์ ๋ง๋ค๊ธฐ ์ํ ํต์ฌ ๊ธฐ๋ฅ์ด๋ค.
ํนํ ๋ฐฑ์คํผ์ค๋ ๋ฐ์ดํฐ๊ฐ ๋ง์์ง์๋ก ๊ฒ์/ํํฐ/์ ๋ ฌ/ํ์ด์ง์ด ์ค์ํ๋ค.
1) ๊ด๋ฆฌ์ ๋ชฉ๋ก ์กฐํ์์ ์ง์ํ ๊ธฐ๋ฅ
์ง์ ํ๋ผ๋ฏธํฐ
- keyword: ์ด๋ฆ/์ด๋ฉ์ผ ๊ฒ์
- role: ์ญํ ํํฐ
- status: ์ํ ํํฐ
- Pageable: ํ์ด์ง + ์ ๋ ฌ
์ฆ, ์ด๋ฐ ์์ฒญ์ด ๊ฐ๋ฅํ๋ค:
- “์ด์ ๊ด๋ฆฌ์๋ง ๋ณด๊ณ ์ถ๋ค”
- “์น์ธ๋๊ธฐ ์ํ๋ง ๋ณด๊ณ ์ถ๋ค”
- “keyword=kim ์ผ๋ก ์ด๋ฆ/์ด๋ฉ์ผ ๊ฒ์”
- “ํ์ด์ง 2๋ฒ, 10๊ฐ์ฉ”
- “createdAt desc ์ ๋ ฌ”
2) Specification์ ์ ํํ ์ด์
๊ด๋ฆฌ์ ๋ชฉ๋ก ์กฐํ๋ ์กฐ๊ฑด์ด ๊ณ์ ๋ณํ๋ค.
- keyword๊ฐ ์์ ์๋ ์์ ์๋
- role๋ ์์ ์๋ ์์ ์๋
- status๋ ์์ ์๋ ์์ ์๋
์ด๊ฑธ if๋ฌธ์ผ๋ก QueryDSL ์์ด ์ฒ๋ฆฌํ๋ฉด:
- ๋ฉ์๋๊ฐ ๊ณผ๋ํ๊ฒ ๋์ด๋๊ฑฐ๋
- ์กฐ๊ฑด ์กฐํฉ๋ง๋ค ์ฟผ๋ฆฌ๋ฅผ ๋ฐ๋ก ๋ง๋ค๊ฒ ๋๋ค.
๊ทธ๋์ JpaSpecificationExecutor + Specification์ผ๋ก
“์กฐ๊ฑด ์กฐํฉ์ ์ ์ฐํ๊ฒ ๋ถ์ด๋ ๋ฐฉ์”์ ํํ๋ค.
3) AdminSpecifications ์์
keyword ๊ฒ์
- keyword๊ฐ ์์ผ๋ฉด null ๋ฐํ → ํด๋น ์กฐ๊ฑด์ ์ ์ฉ ์ ๋จ
- keyword๊ฐ ์์ผ๋ฉด name/email like ๊ฒ์
public static Specification<Admin> keyword(String keyword) {
if (keyword == null || keyword.isBlank()) return null;
return (root, query, cb) -> cb.or(
cb.like(cb.lower(root.get("adminName")), "%" + keyword.toLowerCase() + "%"),
cb.like(cb.lower(root.get("email")), "%" + keyword.toLowerCase() + "%")
);
}
4) AdminQueryService: ์กฐํ ์ ์ฉ ์๋น์ค
@Transactional(readOnly = true)
public Page<AdminListResponse> search(AdminSearchCondition condition, Pageable pageable) {
Specification<Admin> spec = Specification.where(AdminSpecifications.keyword(condition.keyword()))
.and(AdminSpecifications.role(condition.role()))
.and(AdminSpecifications.status(condition.status()));
return adminRepository.findAll(spec, pageable).map(AdminListResponse::from);
}
์ฌ๊ธฐ์ ์ค์ํ ํฌ์ธํธ
โ @Transactional(readOnly = true)
- ์กฐํ๋ readOnly๋ก ์คํ → ์ฑ๋ฅ/์์ ์ฑ ์ธก๋ฉด์์ ์ ๋ฆฌ
- ํ์ ๋ฆฌ๋ทฐ๋ก “ํด๋์ค ๋ ๋ฒจ readOnly” ์ ์ฉ๋ ๋ฐ์ํ์
โ DTO ๋ณํ์ map(AdminListResponse::from)
- Entity๋ฅผ ๊ทธ๋๋ก ๋ฐํํ์ง ์๊ณ ์๋ต DTO๋ก ๋ณํ
- ๋น๋ฐ๋ฒํธ ๊ฐ์ ๋ฏผ๊ฐ ์ ๋ณด ๋ ธ์ถ ๋ฐฉ์ง
5) Controller: searchAdmins
@GetMapping
public ApiResponse<Page<AdminListResponse>> searchAdmins(
@ModelAttribute AdminSearchCondition condition,
Pageable pageable,
HttpSession session
) {
requireSuperAdmin(session);
return ApiResponse.success(HttpStatus.OK, adminQueryService.search(condition, pageable));
}
๋ฆฌ๋ทฐ ๋ฐ์ ํฌ์ธํธ 1) @ModelAttribute
์ฒ์์ keyword, role, status๋ฅผ ๊ฐ๊ฐ ๋ฐ์์ ์ง์ condition์ new๋ก ๋ง๋ค์๋ค.
๊ทผ๋ฐ ํ์ ๋ฆฌ๋ทฐ๋ก:
Spring MVC ๋ฐ์ธ๋ฉ์ ํ์ฉํ๋ฉด new๋ก ์์ฑํ์ง ์์๋ ๋๋ค.
๊ทธ๋์ @ModelAttribute๋ก ์๋ ๋ฐ์ธ๋ฉ์ ์ ์ฉํ๋ค.
๋ฆฌ๋ทฐ ๋ฐ์ ํฌ์ธํธ 2) import ๋๋ฝ ๋ฌธ์
๋ด๊ฐ ํ ๋ฒ ์ค์๋ก AdminStatus๋ฅผ import ์ ํ๊ณ
ํํจํค์ง๋ช
์ ๊ทธ๋๋ก ์ ์ด๋ฒ๋ ธ๋ค.
@RequestParam com.example...AdminStatus status
๋ฆฌ๋ทฐ์์ ๋ฐ๋ก ์ง์ ๋ฐ์๊ณ , import๋ก ์ ๋ฆฌํ๋ค.
WRITE ๊ตฌํ: ์์ /์ญํ /์ํ/์ญ์
WRITE๋ “๊ด๋ฆฌ์ ๊ด๋ฆฌ ํ๋ฉด์์ ๋ฒํผ ๋๋ฅด๋ฉด ์คํ๋๋ ๊ธฐ๋ฅ”์ด๋ค.
- ๊ด๋ฆฌ์ ์ ๋ณด ์์ (์ด๋ฆ/์ด๋ฉ์ผ/์ ํ)
- ์ญํ ๋ณ๊ฒฝ
- ์ํ ๋ณ๊ฒฝ
- ์ญ์
1) ๊ด๋ฆฌ์ ์ ๋ณด ์์ (updateAdmin)
ํต์ฌ ๊ฒ์ฆ: ์ด๋ฉ์ผ ์ค๋ณต ์ฒ๋ฆฌ
์์ ์ ๊ฐ์ ๊ณผ ๋ฌ๋ผ์ ๊ธฐ์กด ์ด๋ฉ์ผ์ด ๊ทธ๋๋ก์ผ ๋๋ ์ค๋ณต ์ฒดํฌ๋ฅผ ํ๋ฉด ์ ๋จ
๊ทธ๋์ ์กฐ๊ฑด์ ์ด๋ ๊ฒ ๋ฃ์๋ค:
if (!admin.getEmail().equals(request.email()) && adminRepository.existsByEmail(request.email())) {
throw new CustomException(ErrorCode.ADMIN_EMAIL_DUPLICATED);
}
โ ์ด๋ฉ์ผ์ด ๋ณ๊ฒฝ๋ ๋๋ง ์ค๋ณต ์ฒดํฌ
2) ์ญํ ๋ณ๊ฒฝ(changeRole)
@Transactional
public void changeRole(Long adminId, AdminRole role) {
Admin admin = findAdmin(adminId);
admin.changeRole(role);
}
- ์ค์ ๊ถํ ์ฒดํฌ๋ Controller์์
- Entity๋ ์ํ ๋ณ๊ฒฝ๋ง ๋ด๋น
3) ์ํ ๋ณ๊ฒฝ(changeStatus)
@Transactional
public void changeStatus(Long adminId, AdminStatus status) {
Admin admin = findAdmin(adminId);
admin.changeStatus(status);
}
4) ์ญ์ (deleteAdmin) - Soft Delete ์ ํ
์ฒ์์ ๋ฌผ๋ฆฌ ์ญ์ ๋ฅผ ํ๋ค.
adminRepository.delete(admin);
๊ทธ๋ฐ๋ฐ ๋ฆฌ๋ทฐ์์:
๊ด๋ฆฌ์ ๊ณ์ ์ ๊ธฐ๋ก ์ถ์ ์ด ์ค์ํ๋ฐ Hard Delete๋ ์ํํ๋ค
status๋ฅผ DELETED๋ก ๋ฐ๊พธ๋ Soft Delete๊ฐ ๋ซ์ง ์๋์?
๊ทธ๋์ Soft Delete๋ก ๋ณ๊ฒฝํ๋ค.
public void delete() {
this.status = AdminStatus.DELETED;
}
์๋น์ค๋:
@Transactional
public void deleteAdmin(Long adminId) {
Admin admin = findAdmin(adminId);
admin.delete();
}
โ
DB row ์ญ์ X
โ
์ํ๋ก๋ง ๋นํ์ฑ ์ฒ๋ฆฌ
โ
์ด์/๊ฐ์ฌ ๋ก๊ทธ ๊ด์ ์์ ์์
ํธ๋ฌ๋ธ์ํ /๋ฆฌ๋ทฐ ๋ฐ์ ์ ๋ฆฌ
์ด๋ฒ ๋จ๊ณ์์ ๊ธฐ๋ฅ ๊ตฌํ๋ณด๋ค
๋ฆฌ๋ทฐ ๋ฐ์์ผ๋ก ์ฝ๋ ํ์ง์ ์ฌ๋ฆฐ ๊ฒฝํ์ด ์ปธ๋ค.
1) ์ธ์ ํค ํ๋์ฝ๋ฉ ์ ๊ฑฐ
์ฒ์์๋ ๋ฌธ์์ด๋ก ์์ฑํ๋๋ฐ
session.getAttribute("LOGIN_SUPER_ADMIN")
๋ฆฌ๋ทฐ๋ก “์์ํ” ๊ถ์ฅ -> SessionConst๋ก ํต์ผํ๋ค.
2) Service์ ํธ๋์ญ์ ์ ๋ต ๊ฐ์
ํ์ ๋ฆฌ๋ทฐ:
ํด๋์ค ๋ ๋ฒจ์ readOnly=true๋ฅผ ๊ฑธ๊ณ
์์ ๋ฉ์๋์๋ง @Transactional ๋ถ์ด๋ฉด ๋๋ฝ ๋ฐฉ์ง๋๋ค.
๊ทธ๋์:
- QueryService -> ํด๋์ค ๋ ๋ฒจ readOnly
- CommandService -> ๊ธฐ๋ณธ readOnly + ์ฐ๊ธฐ๋ง @Transactional
3) ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ๊ฒ์ฆ ๋ก์ง ์์น ๊ฐ์
์๋น์ค๊ฐ ๊ฒ์ฆ์ ๋ค ํ๋ฉด
Service๊ฐ ๋๋ฉ์ธ ๊ท์น์ ๋๋ฌด ๋ง์ด ์๊ฒ ๋๋ค.
๊ทธ๋์ Admin ์ํฐํฐ๋ก ์ฑ ์์ ๋๊ฒผ๋ค.
public void changePasswordWithValidation(...) {
// current mismatch
// new confirm mismatch
// already used password
// encode + ์ ์ฅ
}
๋๋ฉ์ธ ๊ท์น์ ๋๋ฉ์ธ์
์๋น์ค๋ ํ๋ฆ๋ง ์ ์ด
์ค๋ ์์ ์์ฝ
- ๊ด๋ฆฌ์ READ ๊ธฐ๋ฅ ๊ตฌํ
- ๊ฒ์(keyword)
- ํํฐ(role, status)
- ์ ๋ ฌ/ํ์ด์ง(Pageable)
- ๋ชฉ๋ก/์์ธ ์กฐํ API
- ๊ด๋ฆฌ์ WRITE ๊ธฐ๋ฅ ๊ตฌํ
- ๊ด๋ฆฌ์ ์ ๋ณด ์์
- ์ญํ ๋ณ๊ฒฝ
- ์ํ ๋ณ๊ฒฝ
- ์ญ์ (Soft Delete ์ ํ)
- ๋ฆฌ๋ทฐ ๋ฐ์
- import ์ ๋ฆฌ, ํ๋์ฝ๋ฉ ์ ๊ฑฐ
- @ModelAttribute ํ์ฉ
- ํธ๋์ญ์ readOnly ์ ๋ต ๊ฐ์
- ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ๋ก์ง ๋๋ฉ์ธ ์์
ํ๊ณ (Keep / Problem / Try)
โ Keep (๋ง์กฑ)
- admin ํจํค์ง ๊ตฌ์กฐ๋ฅผ ํ ์ปจ๋ฒค์ ์ ๋ง์ถฐ ์ผ๊ด๋๊ฒ ์ ์ง
- Query/Command ๋ถ๋ฆฌ๋ก ์ถฉ๋ ๊ฐ๋ฅ์ฑ ๊ฐ์
- ์๋ต ํฌ๋งท(ApiResponse)๊ณผ ์์ธ ํฌ๋งท(ErrorCode) ์ ์ง
โ ๏ธ Problem (๋ถํธ)
- Soft Delete ์ ์ฉ ํ ์กฐํ ์ ์ธ ์ ์ฑ ์ ๋ ๋ช ํํ๊ฒ ํด์ผ ํจ
- URI prefix(/admins vs /api/admins) ํต์ผ ํ์
๐ Try (๋ค์ ๊ฐ์ )
- Soft Delete ๋ ๊ณ์ ์ ๊ธฐ๋ณธ ์กฐํ์์ ์ ์ธ๋๋๋ก ์ ์ฑ ๊ฐํ
- ๊ด๋ฆฌ ๊ธฐ๋ฅ ์ธํฐ์ ํฐ/ArgumentResolver๋ก ๊ถํ ์ฒดํฌ ๊ณตํตํ
- SuperAdmin ๋๋ฉ์ธ ๋ถ๋ฆฌ ๊ตฌํ (ERD ๊ธฐ๋ฐ)
https://documenter.getpostman.com/view/51133118/2sBXcGDzTV#086ea58f-0aa3-48f1-846e-e32e98d671df