api: Controller, DTO, Facadeλ₯Ό ν¬ν¨νλ©° ν΄λΌμ΄μΈνΈ μμ²μ μ²λ¦¬νλ νλ μ ν μ΄μ κ³μΈ΅controller: RESTful API μλν¬μΈνΈ μ μdto: Request/Response DTO μ μfacade: μ¬λ¬ λλ©μΈ μλΉμ€λ₯Ό μ‘°μ¨νλ Facade ν΄λμ€
domain: λλ©μΈλ³λ‘ ꡬμ±λλ©° κ° λλ©μΈμ λ€μμ νμ ν¨ν€μ§λ₯Ό κ°μ§- Entity ν΄λμ€ (λλ©μΈ λͺ¨λΈ)
application: Service ν΄λμ€μ DTO μ μinfra: JPA Repository ꡬν체 λ° μΈλΆ μμ€ν μ°λ
auth: μΈμ¦/μΈκ° κ΄λ ¨ λ‘μ§ (JWT, OAuth, λ‘κ·ΈμΈ/λ‘κ·Έμμ λ±)external: μΈλΆ API ν΄λΌμ΄μΈνΈ (Feign Client λ±)global: κ³΅ν΅ μ€μ , μμΈ μ²λ¦¬, μ νΈλ¦¬ν°, μ΄λ Έν μ΄μ λ±
- μλΉμ€ κ° μ°Έμ‘° κΈμ§: λλ©μΈ Serviceλ λ€λ₯Έ λλ©μΈμ Serviceλ₯Ό μ§μ μ°Έμ‘°νμ§ μμ
- Facade κ³μΈ΅ νμ: μ¬λ¬ λλ©μΈ Serviceλ₯Ό μ¬μ©ν΄μΌ νλ λΉμ¦λμ€ λ‘μ§μ Facade κ³μΈ΅μμ μ²λ¦¬
- νμ λͺ¨λμ΄ λ§μ κ²½μ° bounded contextλ³λ‘ ν¨ν€μ§ λΆλ¦¬
λλ©μΈλ³λ‘ Serviceλ₯Ό λλλ©΄μ λ°μνλ μλΉμ€ κ° μ°Έμ‘° λ¬Έμ λ₯Ό λ°©μ§νκΈ° μν΄ Facade κ³μΈ΅μ λμ ν¨.
- μ¬λ¬ λλ©μΈμ Serviceλ₯Ό μ‘°μ¨νμ¬ λ³΅μ‘ν λΉμ¦λμ€ λ‘μ§ μ²λ¦¬
- Controllerμ Service μ¬μ΄μ μ€κ° κ³μΈ΅μΌλ‘ λμ
- νΈλμμ
κ²½κ³ μ€μ (νμ μ
@Transactionalμ μ©)
@Facadeμ΄λ Έν μ΄μ μ¬μ©api/{domain}/facadeν¨ν€μ§μ μμΉ- μ¬λ¬ λλ©μΈμ Serviceλ₯Ό μ£Όμ λ°μ μ¬μ© κ°λ₯
- DTO λ³ν λ‘μ§ ν¬ν¨ κ°λ₯
@Facade
@RequiredArgsConstructor
public class SetlistFacade {
private final SetlistService setlistService;
private final SetlistEditService setlistEditService;
private final UserService userService; // λ€λ₯Έ λλ©μΈμ Service μ°Έμ‘°
public SetlistCreateResponseDTO createSetLists(Long userId,
List<SetlistCreateRequestDTO> requests) {
User user = userService.findById(userId); // UserService μ¬μ©
return new SetlistCreateResponseDTO(
setlistService.createSetLists(user, requests) // SetlistService μ¬μ©
);
}
}- JPA Entityλ‘ λλ©μΈ λͺ¨λΈ ꡬν
- λΆλ³μ±κ³Ό μΊ‘μνλ₯Ό μ€μ¬μΌλ‘ μ€κ³
- Setter μ¬μ© μ΅μν (νμν κ²½μ°μλ§
@Setterλͺ μ - μ¬μ© μ΄μ μ£Όμ νμ) - Builder ν¨ν΄ λ° μ μ ν©ν 리 λ©μλ μ¬μ© κΆμ₯ (
@Builder) - μμ±μΌμ (
created_at), μμ μΌμ (updated_at) μΆκ° νμ- JpaAuditing μ¬μ©
- 볡μν μ¬μ© (μ:
users,concerts,festivals) - μ€λ€μ΄ν¬ μΌμ΄μ€(snake_case) μ μ©:
@Table(name = "table_name")
- μ€λ€μ΄ν¬ μΌμ΄μ€ μ μ© (μ:
created_at,user_id) - λΆλ¦¬μΈ:
is_μ λμ¬ μ¬μ© λΆνμ (JPAλ νλλͺ κ·Έλλ‘ λ§€ν) - Enum:
@Enumerated(EnumType.STRING)μ¬μ© κΆμ₯
@GeneratedValue(strategy = GenerationType.IDENTITY)μ¬μ©
- μ§μ° λ‘λ© μ°μ μ¬μ©:
fetch = FetchType.LAZY - μλ°©ν₯ μ°κ΄κ΄κ³ μ μ°κ΄κ΄κ³ νΈμ λ©μλ μμ±
- Cascade νμ μ μ μ€νκ² μ€μ
@CreatedDate,@LastModifiedDateμ¬μ©@EntityListeners(AuditingEntityListener.class)μ μ©
findλ₯Ό κΈ°λ³ΈμΌλ‘ μ¬μ© (μ:findById,findAllByUserId)- λ¨κ±΄ μ‘°ν:
findBy...ννλ‘Optionalλ°ν - λ€κ±΄ μ‘°ν:
findAllBy...λλfindBy...ννλ‘Listλ°ν - μ‘΄μ¬ μ¬λΆ νμΈ:
existsBy...ννλ‘booleanλ°ν - μΉ΄μ΄νΈ:
countBy...ννλ‘longλ°ν
- μ μ₯:
saveμ¬μ© - μμ : JPAμ Dirty Checking νμ© (λ³λ λ©μλ λΆνμ)
- μμ :
deleteλλdeleteByIdμ¬μ©
- Repositoryλ μΈν°νμ΄μ€λ‘ μ μνλ©°
JpaRepositoryμμ - 컀μ€ν
쿼리λ
@Queryμ΄λ Έν μ΄μ μ¬μ© - Repositoryλ
domain/{domain}/infra/repositoryν¨ν€μ§μ μμΉ
getμ¬μ©μ κΆμ₯ (μ:getUserInfo,getSetlistDetail)- null 체ν¬μ μμΈ μ²λ¦¬ νμ
- μ:
repository.findById(id).orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND))
- μ:
- 리μ€νΈ μ‘°ν:
getAll...νν μ¬μ© (μ:getAllMySetlists)
- μμ±:
createμ¬μ© (μ:createSetList) - μμ :
updateλλpatchμ¬μ© - μμ :
deleteμ¬μ©
@Serviceμ΄λ Έν μ΄μ μ¬μ©domain/{domain}/applicationν¨ν€μ§μ μμΉ- νΈλμμ κ΄λ¦¬ νμ (μλ μ°Έμ‘°)
- λͺ¨λ μ°κΈ° μμ
μ
@Transactionalμ μ© - μ½κΈ° μ μ© λ©μλλ
@Transactional(readOnly = true)μ¬μ© - νΈλμμ κ²½κ³λ Service λλ Facade λ©μλ λ¨μλ‘ μ€μ
- κΈ°λ³Έ Propagationμ
REQUIREDμ¬μ©
- μμ±μ μ£Όμ
λ°©μ μ¬μ© (
@RequiredArgsConstructorνμ©) - μν μ°Έμ‘° μ λ κΈμ§ (Facade ν¨ν΄μΌλ‘ ν΄κ²°)
- λΉμ¦λμ€ λ‘μ§μμ λ°μνλ μμΈλ 컀μ€ν μμΈ ν΄λμ€ μ¬μ©
ErrorMessageEnumμ νμ©νμ¬ μμΈ λ©μμ§ κ΄λ¦¬
- λͺ¨λ DTOλ λ°λμ
recordν΄λμ€λ‘ μμ± (νμ) - Requestμ Response DTO λΆλ¦¬
- DTOλ λΆλ³(immutable)νλ©° setter λ©μλ μ λ κΈμ§
- νλ μ κ·Όμ recordμ μλ μμ± accessor λ©μλ μ¬μ© (μ:
id())
// Request DTO μμ
public record LoginRequest(
@NotBlank String provider,
@NotBlank String idToken
) {
}- API μμ²/μλ΅ DTO:
api/{domain}/dto/requestλλapi/{domain}/dto/response - Facade λ΄λΆ DTO:
api/{domain}/facade/dto/requestλλapi/{domain}/facade/dto/response - Domain Service λ΄λΆ DTO:
domain/{domain}/application/dto
- Request DTOμ Bean Validation μ΄λ
Έν
μ΄μ
μ¬μ© (
@NotNull,@NotBlankλ±) - Controller λ©μλ νλΌλ―Έν°μ
@Validμ μ©
API μλν¬μΈνΈμ λν κΆν κΈ°λ° μ κ·Ό μ μ΄λ₯Ό μνν¨.
- Controller λ©μλμλ§ μ μ© κ°λ₯ (
@Target(ElementType.METHOD))
- HTTP μμ²μ
Authorizationν€λμμ JWT ν ν° μΆμΆ - ν ν°μμ μ¬μ©μμ Role μ 보 νμ±
- λ©μλμ μ§μ λ νμ© Roleκ³Ό λΉκ΅νμ¬ μ κ·Ό κΆν κ²μ¦
- κΆνμ΄ μμΌλ©΄
FORBIDDENμμΈ λ°μ
role: νμ©ν Role λ°°μ΄ (κΈ°λ³Έκ°:Role.ONBOARDING)Role.ONBOARDING: μ¨λ³΄λ© μ€μΈ μ¬μ©μRole.GENERAL: μΌλ° μ¬μ©μRole.ADMIN: κ΄λ¦¬μ
@Permission(role = {Role.GENERAL})
@GetMapping("/user/info")
public ResponseEntity<BaseResponse<UserInfoResponse>> getUserInfo(@UserId Long userId) {
// Role.GENERAL κΆνμ κ°μ§ μ¬μ©μλ§ μ κ·Ό κ°λ₯
}
@Permission(role = {Role.ONBOARDING, Role.GENERAL})
@PostMapping("/auth/reissue")
public ResponseEntity<BaseResponse<Token>> reissue(@RefreshToken String refreshToken) {
// Role.ONBOARDING λλ Role.GENERAL κΆνμ κ°μ§ μ¬μ©μ μ κ·Ό κ°λ₯
}| μ½λ | μλ΅λͺ | μ¬μ© μ©λ |
|---|---|---|
| 200 | OK | μμ²μ΄ μ μμ μΌλ‘ μ²λ¦¬λ¨ (ApiResponseUtil.success() μ¬μ©) |
| 201 | Created | 리μμ€κ° μ±κ³΅μ μΌλ‘ μμ±λ¨ (ResponseEntity.created(uri).body() μ¬μ©) |
| 204 | No Content | μμ² μ±κ³΅, λ°νν μ½ν
μΈ μμ (ResponseEntity.noContent().build() μ¬μ©) |
| 400 | Bad Request | μλͺ»λ μμ² (@Valid κ²μ¦ μ€ν¨ λ±) |
| 401 | Unauthorized | μΈμ¦ νμ (JWT ν ν° κ²μ¦ μ€ν¨) |
| 403 | Forbidden | κΆν μμ (@Permission κΆν κ²μ¦ μ€ν¨) |
| 404 | Not Found | 리μμ€ μμ (Entity μ‘°ν μ€ν¨) |
| 405 | Method Not Allowed | μ§μνμ§ μλ HTTP λ©μλ |
| 409 | Conflict | 리μμ€ μν μΆ©λ (μ€λ³΅ μμ± μλ λ±) |
| 422 | Unprocessable Entity | μλ―Έμ μ€λ₯ (λΉμ¦λμ€ κ·μΉ μλ°) |
| 500 | Internal Server Error | μλ² λ΄λΆ μ€λ₯ (μ²λ¦¬λμ§ μμ μμΈ) |
- λͺ¨λ μλ¬ μ½λλ
ErrorMessageEnumμΌλ‘ κ΄λ¦¬ - Enum μ΄λ¦:
UPPER_SNAKE_CASEμ¬μ© - λ©μμ§: νκ΅μ΄λ‘ μμ± (νλ‘μ νΈ μ μ± )
- κ°κ²°νκ³ λͺ ννκ²: μ¬μ©μκ° μ΄ν΄νκΈ° μ¬μ΄ λ©μμ§ μμ±
- μ€ν κ°λ₯ν μ 보: κ°λ₯ν κ²½μ° λ¬Έμ ν΄κ²°μ λμμ΄ λλ μ 보 ν¬ν¨
- κΈ°μ μ μΈλΆμ¬ν μ μΈ: ꡬν μΈλΆμ¬νμ΄λ μ€ν νΈλ μ΄μ€ λ ΈμΆ κΈμ§
- ErrorMessageλ₯Ό λ°λ μμΈ ν΄λμ€λ₯Ό κ° μν©μ λ§κ² ꡬννμ¬ μ¬μ©
- κ³΅ν΅ μμΈ:
ConfetiException(νλ‘μ νΈλͺ κΈ°λ°) - νΉμ μμΈ:
NotFoundException,UnauthorizedException,ConflictExceptionλ±
@RestControllerAdviceλ₯Ό μ¬μ©νμ¬ μ μ μμΈ μ²λ¦¬- κ° μμΈ νμ
λ³λ‘
@ExceptionHandlerλ©μλ μ μ - μμΈ μ 보λ₯Ό request attributeμ μ μ₯νμ¬ λ‘κΉ κ°λ₯
ApiResponseUtil.failure()λ₯Ό μ¬μ©νμ¬ μΌκ΄λ μλ¬ μλ΅ μμ±
- μ€λ€μ΄ν¬ μΌμ΄μ€(snake_case) μ μ©: μλ¬Έμμ μΈλμ€μ½μ΄ μ‘°ν© (μ:
user_account) - 볡μν μ¬μ© (μ:
users,concerts,festivals)
- μ€λ€μ΄ν¬ μΌμ΄μ€ μ μ© (μ:
created_at,user_id) - λΆλ¦¬μΈ:
is_μ λμ¬ μ¬μ©νμ§ μμ (JPAλ νλλͺ κ·Έλλ‘ λ§€ν) - μ½μ΄λ³΄λ€ μ 체 λ¨μ΄ μ¬μ©:
descλμdescriptionκΆμ₯ - λ°μ΄ν° νμ
μ¬μ© κΈμ§: μ»¬λΌ μ΄λ¦μ λ°μ΄ν° νμ
ν¬ν¨ κΈμ§ (μ:
text,int) - Enum 컬λΌ:
VARCHARνμ μΌλ‘ Enum μ΄λ¦ μ μ₯ (@Enumerated(EnumType.STRING))
BIGINT+AUTO_INCREMENT(MySQL)- JPA:
@GeneratedValue(strategy = GenerationType.IDENTITY)
- μ§§μ ν
μ€νΈ:
VARCHAR(n)(κΈΈμ΄ μ νμ΄ λͺ νν κ²½μ°) - κΈ΄ ν
μ€νΈ:
TEXTνμ μ¬μ© κ°λ₯
created_atμupdated_atλ νμλ‘ κ΅¬μ±DATETIMEλλTIMESTAMPνμ μ¬μ©- JPA:
@CreatedDate,@LastModifiedDateμ¬μ©
- μΈλν€ μ»¬λΌ:
{μ°Έμ‘° ν μ΄λΈλͺ }_idνμ (μ:user_id,concert_id) - JPA:
@JoinColumn(name = "...")λͺ μ
- μΌλ° μΈλ±μ€:
idx_{table}_{column}(μ:idx_user_email) - μ λν¬ μΈλ±μ€:
uidx_{table}_{column} - λ³΅ν© μΈλ±μ€:
idx_{table}_{column1}_{column2}
- Redis Sentinelμ μ¬μ©ν κ³ κ°μ©μ± ꡬμ±
- Master-Slave 볡μ ꡬ쑰
- Lettuce ν΄λΌμ΄μΈνΈ μ¬μ©
REPLICA_PREFERREDμ½κΈ° μ λ΅: Replica μ°μ , Replica λΆκ° μ Masterμμ μ½κΈ°
- Command Timeout, Connect Timeout, Shutdown Timeout μ€μ νμ
- μ°κ²° κ²μ¦ νμ±ν (
setValidateConnection(true))
- Refresh Token μΊμ±
- μΈμ λ°μ΄ν° μ μ₯
- μμ λ°μ΄ν° μΊμ±
- Keyμ Value λͺ¨λ String μ§λ ¬ν μ¬μ©
- 컀μ€ν μ§λ ¬νκ° νμν κ²½μ° λ³λ μ€μ
- λͺ¨λ Controllerλ λ°λμ Docs μΈν°νμ΄μ€λ₯Ό ꡬννλ λ°©μ μ¬μ©
- Controllerμ API λ¬Έμλ₯Ό λΆλ¦¬νμ¬ κ°λ μ± ν₯μ
- μΈν°νμ΄μ€μ Swagger μ΄λ Έν μ΄μ μ μ, Controllerλ λΉμ¦λμ€ λ‘μ§μλ§ μ§μ€
- API μλν¬μΈνΈ μ€λͺ μ νκ΅μ΄λ‘ μμ±
- λ΄λΆ μ£Όμμ΄λ μ½λ λ΄ μ€λͺ λ νκ΅μ΄ μ¬μ© κ°λ₯
Swaggerμ μ λ°μ μΈ μ€μ μ SwaggerConfig ν΄λμ€μμ κ΄λ¦¬.
- API μ 보 (μ λͺ©, λ²μ , μ€λͺ , μ°λ½μ²)
- μλ² URL (λ‘컬 κ°λ°, μ΄μ λ±)
- 보μ μ€ν€λ§ (JWT Bearer μΈμ¦)
addPermissionDescriptionλ©μλλ‘ API μ€λͺ μ νμ κΆν μλ μΆκ°- μ:
π **Required Permissions:** GENERAL
api/{domain}/controller/docs/{ControllerName}Docs.java κ²½λ‘μ μΈν°νμ΄μ€ μμ±
Controller ν΄λμ€μμ Docs μΈν°νμ΄μ€λ₯Ό implementsνμ¬ κ΅¬ν
- μΈν°νμ΄μ€ νμ© νμ: Controllerμ λ¬Έμ λΆλ¦¬λ₯Ό μν΄ λ°λμ μΈν°νμ΄μ€ μ¬μ©
- νκ΅μ΄ μμ±:
summaryμdescriptionμ νκ΅μ΄λ‘ μμ± - μλ΅ μ€ν€λ§ μ μ: μλ΅ λ°μ΄ν° ν΄λμ€λ₯Ό λͺ μνμ¬ API νΈμΆμκ° λ°νκ° κ΅¬μ‘°λ₯Ό μ½κ² μ΄ν΄νλλ‘ ν¨
- κ³΅ν΅ μλ¬ μλ΅:
@AuthErrorResponses,@CommonErrorResponsesλ± μ»€μ€ν μ΄λ Έν μ΄μ νμ©
κ³΅ν΅ μλ¬ μλ΅μ 컀μ€ν μ΄λ Έν μ΄μ μΌλ‘ μ μνμ¬ μ¬μ¬μ©μ± ν₯μ
- κ°λ₯ν μ§§κ² μμ± (30μ€ μ΄λ΄ κΆμ₯)
- 볡μ‘ν λ‘μ§μ private λ©μλλ‘ λΆλ¦¬
- "λ
Έλ"λ₯Ό μλ―Ένλ κ²½μ°
songμΌλ‘ νκΈ°ν΄μΌ ν¨.- λ¨,
Apple Music APIμ κ°μ μλΉμ€λͺ μmusicν€μλλ μμΈ
- λ¨,
μμ λμ μλ―Έλ₯Ό μ§λ κ²½μ°Upcomingν€μλκ° λ©μλ λͺ , λ³μ λͺ , ν΄λμ€ λͺ μ λ°λμ λ€μ΄κ°μΌ ν¨.