์ด์ง€๋ฏผ์˜ ๊ฐœ๋ฐœ ๋…ธํŠธ

๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์„ ๊ณต๋ถ€ํ•˜๋ฉฐ Java์™€ Spring์„ ์ค‘์‹ฌ์œผ๋กœ ํ•™์Šต ๊ธฐ๋ก์„ ์ •๋ฆฌํ•˜๋Š” ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค.

๐Ÿ”ฅ ๋‚ด์ผ๋ฐฐ์›€์บ ํ”„/๐Ÿ“Œ ๊ณผ์ œ

[๋‚ด์ผ๋ฐฐ์›€์บ ํ”„ Spring 3๊ธฐ] ๊ณ ๊ฐ ๋ฐ ์ƒํ’ˆ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ํ”„๋กœ์ ํŠธ (์ด์ปค๋จธ์Šค ๋ฐฑ์˜คํ”ผ์Šค) - ๊ด€๋ฆฌ์ž ๊ด€๋ฆฌ ๊ธฐ๋Šฅ

jiiimni 2026. 3. 10. 12:36

๐Ÿง‘‍๐Ÿ’ป 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