[Spring] ๋ฐฑ์คํผ์ค ๊ด๋ฆฌ์ ์ธ์ฆ(์ธ์ ) ๊ตฌํ ํ๊ณ & ํธ๋ฌ๋ธ์ํ (ํ์๊ฐ์ /๋ก๊ทธ์ธ/๋ก๊ทธ์์/์ธ์ ์ค์ )

ํ ํ๋ก์ ํธ์์ “๋ฐฑ์คํผ์ค ๊ด๋ฆฌ์” ํํธ๋ฅผ ๋งก๊ฒ ๋๋ฉด์, ํ์ ๊ธฐ๋ฅ ์ค ์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ(์ฟ ํค/์ธ์ )์ ๋จผ์ ๊ตฌํํ๋ค.
๊ตฌํํ ๊ธฐ๋ฅ ๋ชฉ๋ก (ํ์ 4๊ฐ)
- ๊ด๋ฆฌ์ ํ์๊ฐ์ API (๊ธฐ๋ณธ ์ํ: ์น์ธ๋๊ธฐ PENDING, ๋น๋ฐ๋ฒํธ ์ํธํ ์ ์ฅ)
- ๊ด๋ฆฌ์ ๋ก๊ทธ์ธ API (์ธ์ ์์ฑ ๋ฐ ์ ์ฅ, ACTIVE๋ง ๋ก๊ทธ์ธ ๊ฐ๋ฅ)
- ๊ด๋ฆฌ์ ๋ก๊ทธ์์ API (์ธ์ invalidate)
- ์ธ์ ๋ง๋ฃ ์๊ฐ/์ฟ ํค ์ต์ (HttpOnly) ์ค์
1. ํ ์ปจ๋ฒค์ ์ ๋ง์ถ ํจํค์ง ๊ตฌ์กฐ
ํ ํ๋ก์ ํธ๋ผ์ ๋ด ๋ง์๋๋ก ๊ตฌ์กฐ๋ฅผ ๋ง๋ค๋ฉด ๋์ค์ ์ถฉ๋์ด ๋๊ฑฐ๋ ์ ์ง๋ณด์๊ฐ ์ด๋ ค์์ง ์ ์๋ค.
ํ๋ก์ ํธ ๋ด๋ถ๋ฅผ ๋ณด๋ ๋๋ฉ์ธ๋ณ๋ก ๋ฌถ๋ ๋ฐฉ์์ด ์ฌ์ฉ๋๊ณ ์์๊ณ , ๊ณตํต ์๋ต/์์ธ ํฌ๋งท๋ ์ด๋ฏธ ์กด์ฌํ๋ค.
๊ทธ๋์ ๊ด๋ฆฌ์ ์ธ์ฆ์ ์๋์ฒ๋ผ admin ๋๋ฉ์ธ ํจํค์ง๋ฅผ ๋ง๋ค๊ณ ๊ทธ ์์์ 3-layer๋ฅผ ์ ์งํ๋ค.
com.example.commercepilot
โฃ admin
โ โฃ controller
โ โฃ dto
โ โ โฃ request
โ โ โฃ response
โ โ โ session
โ โฃ entity
โ โฃ repository
โ โฃ service
โฃ config
โฃ exception
ํ ๊ณตํต ํฌ๋งท ์ฌ์ฌ์ฉ
- ApiResponse (์ฑ๊ณต/์คํจ ์๋ต ๊ณตํต)
- ErrorCode, ErrorResponse
- GlobalExceptionHandler (CustomException + Validation ์ฒ๋ฆฌ)
ํ ์ปจ๋ฒค์ ์ ์ต๋ํ ๋ฐ๋ผ๊ฐ๋ฉด์, ๋ด๊ฐ ๊ตฌํํ๋ ๊ธฐ๋ฅ๋ง ์์ฐ์ค๋ฝ๊ฒ ์ถ๊ฐ๋๋๋ก ํ๋ค.
2. ๊ด๋ฆฌ์ ํ์๊ฐ์ API ๊ตฌํ
2-1. ์๊ตฌ์ฌํญ ํต์ฌ ์ ๋ฆฌ
ํ์๊ฐ์ ์์ ๋์น๋ฉด ์ ๋๋ ํฌ์ธํธ๋ ์๋์๋ค.
- ์ ๋ ฅ๊ฐ ์ ํจ์ฑ ๊ฒ์ฆ (์ด๋ฉ์ผ ํ์, ๋น๋ฐ๋ฒํธ ์ต์ 8์, ์ ํ๋ฒํธ 010-XXXX-XXXX, role ํ์ ๋ฑ)
- ์ด๋ฉ์ผ ์ค๋ณต ์ฒดํฌ
- ๋น๋ฐ๋ฒํธ ์ํธํ ์ ์ฅ (BCrypt ๊ธฐ๋ฐ PasswordEncoder)
- ๊ฐ์ ์ ๊ธฐ๋ณธ ์ํ: ์น์ธ๋๊ธฐ(PENDING)
2-2. DTO(Validation) ์ค๊ณ ์ด์
ํ์๊ฐ์
์์ฒญ DTO๋ record๋ก ๋ง๋ค๊ณ Bean Validation์ ๋ถ์๋ค.
Controller์์ @Valid๋ฅผ ๋ถ์ด๋ฉด Validation ์คํจ ์ MethodArgumentNotValidException์ด ๋ฐ์ํ๊ณ , ํ ๊ณตํต GlobalExceptionHandler์์ ApiResponse.fail(INVALID_INPUT_VALUE, fieldErrors) ํํ๋ก ๋ด๋ ค์ค ์ ์๋ค.
public record AdminSignupRequest(
@NotBlank String name,
@Email @NotBlank String email,
@Size(min = 8) @NotBlank String password,
@Pattern(regexp = "^010-\\d{4}-\\d{4}$") @NotBlank String callNumber,
@NotNull AdminRole role
) {}
์๋ต DTO๋ ๋ฏผ๊ฐ์ ๋ณด(๋น๋ฐ๋ฒํธ)๋ ์ ๋ ํฌํจํ์ง ์๋๋ก ์ต์ ํ๋๋ง ์ ์ํ๋ค.
public record AdminSignupResponse(
Long id,
String name,
String email,
String callNumber,
AdminRole role,
AdminStatus status,
LocalDateTime createdAt
) { ... }
๐ ์ password๋ฅผ ์๋ต์ ์ ๋ฃ๋๊ฐ?
๋ฆฌ๋ทฐ์์๋ ์ค์ ๋ก ๋์จ ํฌ์ธํธ์ธ๋ฐ, ํด์๋ ๋น๋ฐ๋ฒํธ๋ผ๋ ์๋ต์ผ๋ก ๋ด๋ ค์ฃผ๋ ๊ฑด ๋ณด์์ ์ ์ ํ์ง ์๋ค.
๊ทธ๋์ ์๋ต DTO์ password๋ ์์ ํฌํจ์ํค์ง ์์๋ค.
2-3. Service ๋ก์ง ์ค๊ณ ์ด์ (CommandService)
ํ์๊ฐ์
์ “๋ฐ์ดํฐ๋ฅผ ์์ฑํ๋(์ฐ๊ธฐ)” ๋ก์ง์ด๋ค.
๊ทธ๋์ ํ์์ ์ถ์ฒํ๋ CQRS ๋ง๋ณด๊ธฐ ๊ตฌ์กฐ์ฒ๋ผ, ์ฐ๊ธฐ ๋ก์ง์ CommandService์ ๋๊ณ ํ์ฅ ๊ฐ๋ฅํ๋๋ก ๊ตฌ์ฑํ๋ค.
@Transactional
public AdminSignupResponse signup(AdminSignupRequest request) {
if (adminRepository.existsByEmail(request.email())) {
throw new CustomException(ErrorCode.ADMIN_EMAIL_DUPLICATED);
}
String encoded = passwordEncoder.encode(request.password());
Admin admin = Admin.pending(
request.name(),
request.email(),
encoded,
request.callNumber(),
request.role()
);
Admin saved = adminRepository.save(admin);
return AdminSignupResponse.from(saved);
}
์ฌ๊ธฐ์ ๋ด๊ฐ ์ ๊ฒฝ์ด ๋ถ๋ถ
- ์ค๋ณต ์ด๋ฉ์ผ์ existsByEmail๋ก ๋น ๋ฅด๊ฒ ์ฒดํฌ
- ๋น๋ฐ๋ฒํธ๋ PasswordEncoder.encode()๋ก ์ ์ฅ ์ง์ ์ ์ํธํ
- ์ํ๋ ๋ฌด์กฐ๊ฑด PENDING์ผ๋ก ๊ณ ์ (์น์ธ ์ ์๋ ๋ก๊ทธ์ธ ๋ถ๊ฐ)
3. ๋ก๊ทธ์ธ API ๊ตฌํ (์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ)
3-1. ์๊ตฌ์ฌํญ ํต์ฌ
๋ก๊ทธ์ธ์ ๋จ์ํ ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ๋ง ๋ง์ผ๋ฉด ๋์ด ์๋๋ผ, ๊ณ์ ์ํ์ ๋ฐ๋ผ ๋ก๊ทธ์ธ ๊ฐ๋ฅ ์ฌ๋ถ๊ฐ ๊ฐ๋ฆฐ๋ค.
- ACTIVE๋ง ๋ก๊ทธ์ธ ๊ฐ๋ฅ
- PENDING/REJECTED/SUSPENDED/INACTIVE๋ ๋ก๊ทธ์ธ ๋ถ๊ฐ + ๋ช ํํ ๋ฉ์์ง
๋ํ ๋ก๊ทธ์ธ ์ฑ๊ณต ์์๋ ์๋ฒ ์ธ์ ์ ์๋ ์ ๋ณด๋ฅผ ์ ์ฅํด์ผ ํ๋ค.
- adminId
- role
3-2. ์ธ์ ์ ์ฅ DTO + ์ธ์ ํค ์์
์ธ์ ์ ์ ์ฅํ๋ ๋ฐ์ดํฐ๋ ์ต์ํ์ผ๋ก, ๊ทธ๋ฆฌ๊ณ ๋ช ํํ๊ฒ ํ๋ ค๊ณ ๋ณ๋ record๋ก ๋บ๋ค.
public record LoginAdmin(Long adminId, String email, AdminRole role) {}
์ธ์ key๋ ์คํ ๋ฐฉ์ง ์ํด ์์๋ก ๊ด๋ฆฌํ๋ค.
public final class SessionConst {
public static final String LOGIN_ADMIN = "LOGIN_ADMIN";
}
3-3. ๋ก๊ทธ์ธ Service ๋ก์ง (์ํ๋ณ ๋ถ๊ธฐ)
@Transactional(readOnly = true)
public void login(AdminLoginRequest request, HttpSession session) {
Admin admin = adminRepository.findByEmail(request.email())
.orElseThrow(() -> new CustomException(ErrorCode.LOGIN_FAILED));
if (!passwordEncoder.matches(request.password(), admin.getPassword())) {
throw new CustomException(ErrorCode.LOGIN_FAILED);
}
validateLoginStatus(admin.getStatus());
LoginAdmin loginAdmin = new LoginAdmin(admin.getId(), admin.getEmail(), admin.getRole());
session.setAttribute(SessionConst.LOGIN_ADMIN, loginAdmin);
}
๐ ์ ์ด๋ฉ์ผ ์์ / ๋น๋ฒ ํ๋ฆผ์ ๊ฐ์ ์๋ฌ(LOGIN_FAILED)๋ก ์ฒ๋ฆฌํ๋?
๋ณด์์ “์ด๋ฉ์ผ์ด ์กด์ฌํ๋์ง”๋ฅผ ๋
ธ์ถํ๋ฉด ์ ๋๊ธฐ ๋๋ฌธ์ ๋ณดํต ๋ก๊ทธ์ธ ์คํจ๋ ๋์ผํ๊ฒ ์ฒ๋ฆฌํ๋ค.
๊ทธ๋์ ๊ณ์ ์กด์ฌ ์ฌ๋ถ๋ ์จ๊ธฐ๊ณ , ์ํ(์น์ธ๋๊ธฐ/์ ์ง ๋ฑ)๋ ์๊ตฌ์ฌํญ์ ๋ง๊ฒ ๋ช
ํํ ๋ถ๊ธฐํ๋ค.
4. ๋ก๊ทธ์์ API ๊ตฌํ
๋ก๊ทธ์์์ ๊ฐ๋จํ ์ธ์ ๋ฌดํจํ๋ง ํ๋ฉด ๋๋ค.
public void logout(HttpSession session) {
session.invalidate();
}
์ปจํธ๋กค๋ฌ์์๋ ์๋์ฒ๋ผ ์๋ต data ์์ด ์ฑ๊ณต๋ง ๋ด๋ ค์ฃผ๋๋ก ํ๋ค.
@PostMapping("/logout")
public ApiResponse<Void> logout(HttpSession session) {
adminAuthService.logout(session);
return ApiResponse.success(HttpStatus.OK);
}
5. ์ธ์ ๋ง๋ฃ์๊ฐ/์ฟ ํค(HttpOnly) ์ค์
์๋ฒ์์ ์ธ์
์ ์ค๋ ์ ์งํ ์ง ๊ฒฐ์ ํด์ผ ํ๋ค.
ํ์ ์๊ตฌ์ฌํญ ์์๊ฐ 24์๊ฐ์ด์๊ณ , ์ธ์
์ฟ ํค๋ XSS ์ํ์ ์ค์ด๊ธฐ ์ํด HttpOnly ์ต์
์ ์ฌ์ฉํ๋ค.
application.properties
server.servlet.session.timeout=24h
server.servlet.session.cookie.http-only=true
6. ํธ๋ฌ๋ธ์ํ
6-1. DTO ํจํค์ง ๊ตฌ์กฐ ๋๋ฌธ์ “import ์ค๋ฅ”๊ฐ ๋ฌ๋ค
์ฒ์์๋ DTO๋ฅผ admin/dto ์๋์ ๋ฐ๋ก ๋๊ณ import๋ฅผ ์์ฑํ๋๋ฐ, ํ ๊ตฌ์กฐ๋ฅผ ๋ฐ๋ผ๊ฐ๋ค ๋ณด๋ dto/request, dto/response๋ก ํด๋๋ฅผ ๋๋ด๋ค.
๊ทธ ์ํ์์ ์๋น์ค ์ฝ๋๊ฐ ์๋์ฒ๋ผ ์์ฑ๋์ด ์์ผ๋ฉด:
import com.example.commercepilot.admin.dto.AdminSignupRequest;
์ค์ DTO ๊ฒฝ๋ก๋:
com.example.commercepilot.admin.dto.request.AdminSignupRequest
์ด๋ผ์ IntelliJ์์ Cannot resolve symbol ์๋ฌ๊ฐ ๊ณ์ ๋ฌ๋ค.
ํด๊ฒฐ:
- DTO ํ์ผ์ package ์ ์ธ์ ํด๋ ๊ตฌ์กฐ์ ๋ง์ถ๊ณ ,
- Service/Controller import๋ ์ ํํ ๋ง์ถฐ์คฌ๋ค.
6-2. “์ํธํ๋ ๋น๋ฐ๋ฒํธ๋ฅผ ์๋ต์ ๋ด๋ ๊ฒ ์ ์ ํ๊ฐ์?” ๋ฆฌ๋ทฐ ๋์
์๋น์ค ๋ก์ง์ encoded ๋ณ์๊ฐ ๋ณด์ด๋ค ๋ณด๋, ๋ฆฌ๋ทฐ์ด๊ฐ “์๋ต์ password๊ฐ ํฌํจ๋๋์ง”๋ฅผ ๊ฑฑ์ ํด์ฃผ์ จ๋ค.
์ค์ ๋ก๋ AdminSignupResponse์ password ํ๋๊ฐ ์๊ณ , from()์์๋ ๋งคํํ์ง ์์ ์๋ต์ผ๋ก ๋๊ฐ์ง ์๋๋ค.
โ ๋์:
- AdminSignupResponse์ password๊ฐ ์์์ ์ฝ๋๋ก ์ค๋ช
- “ํด์๋ผ๋ ์๋ต์ ํฌํจํ์ง ์๋ ๊ฒ์ด ์์น”์ด๋ผ๋ ์ ์ ํ์ธ
(์ถ๊ฐ๋ก ์์ ํ๊ฒ ํ๋ ค๋ฉด ์ํฐํฐ password์ @JsonIgnore๋ฅผ ๋ถ์ฌ ์ค์๋ก ์ํฐํฐ๋ฅผ ๋ฐํํด๋ ๋ ธ์ถ๋์ง ์๊ฒ ๋ง์ ์๋ ์๋ค.)
7. ํ๊ณ
์ด๋ฒ ์ธ์ฆ ํํธ๋ฅผ ํ๋ฉด์ ๋๋ ๊ฑด, ๊ธฐ๋ฅ ์์ฒด๋ณด๋ค ํ ์ปจ๋ฒค์ ์ ๋ง์ถฐ ๊ตฌ์กฐ๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ๋ผ์ ๋ฃ๋ ๊ฒ์ด ๋ ์ค์ํ๋ค๋ ์ ์ด๋ค.
- “๋ด๊ฐ ์ต์ํ ๊ตฌ์กฐ”๋๋ก ๋ง๋ค๋ฉด ๋นจ๋ฆฌ ๋ง๋ค ์ ์์ง๋ง,
- ํ ํ๋ก์ ํธ๋ ๊ฒฐ๊ตญ ๋ค ๊ฐ์ด ์ ์ง๋ณด์ํ ์ ์๋ ๊ตฌ์กฐ๊ฐ ๋ ๊ฐ์น๊ฐ ํฌ๋ค.
๋ํ ๋ก๊ทธ์ธ/ํ์๊ฐ์ ์ ์ค๋ฌด์์๋ ๋ฏผ๊ฐํ ์์ญ์ด๋ผ
- ์๋ต์ ๋ฏผ๊ฐ์ ๋ณด ํฌํจ ์ฌ๋ถ
- ์คํจ ์๋ต ๋ฉ์์ง ์ ์ฑ
- ์ธ์
๋ณด์ ์ต์
(HttpOnly)
๊ฐ์ ์ธ๋ถ ํฌ์ธํธ๊ฐ ์ค์ํ๋ค๋ ๊ฑธ ์ฒด๊ฐํ๋ค.
8. ๋ค์ ํ ์ผ
์ธ์ฆ(์ธ์ ) 4๊ฐ๋ฅผ ๋๋ธ ๋ค์๋ ๊ด๋ฆฌ์ ๊ธฐ๋ฅ์ด ๋ณธ๊ฒฉ์ ์ผ๋ก ๋จ์์๋ค.
- ๊ด๋ฆฌ์ ์น์ธ/๊ฑฐ๋ถ(์ํผ๊ด๋ฆฌ์ ์ธ์ ๊ธฐ๋ฐ)
- ๋ด ํ๋กํ ์กฐํ/์์
- ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ
- ๊ด๋ฆฌ์ ๋ชฉ๋ก/๊ฒ์/์ ๋ ฌ/ํํฐ/ํ์ด์ง
- ๊ด๋ฆฌ์ ์์ธ/์์ /์ญ์ /์ญํ ๋ณ๊ฒฝ/์ํ ๋ณ๊ฒฝ
๋ค์ ๊ธ์์๋ “์ํผ๊ด๋ฆฌ์ ์น์ธ ํ๋ก์ธ์ค(์น์ธ๋๊ธฐ -> ํ์ฑ)”์ ๊ตฌํํ๋ฉด์ ์๊ธด ์ด์๋ค์ ์ ๋ฆฌํ ์์ ์ด๋ค.