๋ฉํ ๊ฐ ๋ฉํฐ์๊ฒ ๊ณผ์ ๋ฅผ ๋ถ์ฌํ๊ณ , ํ๋๋๋ฅผ ๊ฒํ ํ๋ฉฐ ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ ํ์ต ์ฝ์นญ ํ๋ซํผ์ ๋ฐฑ์๋์ ๋๋ค.
- Stack: Spring Boot 3.5, Java 21, MySQL 8.4, JPA(Hibernate), Spring Security
- Infra: AWS S3(ํธํ ์คํ ๋ฆฌ์ง), Docker Compose, Railway ๋ฐฐํฌ
ํ์ฌ ๋ฐฑ์๋ ์๋ฒ๋ Railway์ ๋ฐฐํฌ๋์ด ์์ผ๋ฉฐ, Swagger UI์์ API ๋ช ์ธ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
| ํญ๋ชฉ | URL |
|---|---|
| Production Server | https://back-end-production-4beb.up.railway.app |
| Swagger UI | https://back-end-production-4beb.up.railway.app/swagger-ui/index.html |
๋๋ฉ์ธ ์ฃผ๋ ํจํค์ง ๊ตฌ์กฐ๋ก, ๊ฐ ๋๋ฉ์ธ์ด controller / dto / entity / repository / service๋ฅผ ์์ฒด์ ์ผ๋ก ๋ณด์ ํฉ๋๋ค.
com.seolstudy.backend
โโโ domain
โ โโโ auth # JWT ์ธ์ฆ (ํ์๊ฐ์
/๋ก๊ทธ์ธ/ํ ํฐ ์ฌ๋ฐ๊ธ/๋ก๊ทธ์์)
โ โโโ user # ์ฌ์ฉ์ + ์ญํ (MENTOR/MENTEE)
โ โโโ mentoring # ๋ฉํ -๋ฉํฐ ๋งค์นญ ๊ด๋ฆฌ
โ โโโ task # ๊ณผ์ ๋ถ์ฌ(๋ฉํ ) / ์ ์ถ(๋ฉํฐ) + ์๋ฃ ์ธ์ฆ
โ โโโ planner # ์ผ์๋ณ ํ๋๋ + ๋ฉํ ์ฝ๋ฉํธ
โ โโโ feedback # ์ผ์๋ณ ๋ฉํ ํผ๋๋ฐฑ
โ โโโ subject # ๊ณผ๋ชฉ (์์ ์ ์๋)
โโโ global
โโโ config # Security, S3, Swagger, Web, Cors
โโโ security # JwtTokenProvider, JwtAuthenticationFilter, UserDetails
โโโ payload # CommonResponse + Error/Success Status enum
โโโ exception # GeneralException + GlobalExceptionHandler
โโโ storage # FileStorage ์ถ์ํ (S3 / Local)
์
๋ก๋ ๋ก์ง์ FileStorage ์ธํฐํ์ด์ค๋ก ์ถ์ํํ๊ณ , Spring @Profile๋ก ๊ตฌํ์ฒด๋ฅผ ์๋ ์ ํํฉ๋๋ค.
๋น์ฆ๋์ค ์๋น์ค๋ ๊ตฌํ์ฒด(S3/๋ก์ปฌ)๋ฅผ ์ ํ ๋ชจ๋ฅธ ์ฑ ์ธํฐํ์ด์ค์๋ง ์์กดํฉ๋๋ค.
[PlannerService / TaskService] โ ์ธํฐํ์ด์ค์๋ง ์์กด
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ FileStorage โ (interface)
โโโโโโโโโโโโโโโโโโโโ
โฒ โฒ
@Profile("!local") โ @Profile("local")
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
โS3FileStorageโ โLocalFileStorageAdapterโโโ์์โโโถ LocalFileStorage
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ (์ค์ ํ์ผ I/O)
ํต์ฌ ์ค๊ณ
| ํญ๋ชฉ | ๋ด์ฉ | ๊ทผ๊ฑฐ |
|---|---|---|
| ์ ๋ต ํจํด + DIP | ํ๋กํ์ด ์ฃผ์ ๋น์ ๊ฒฐ์ โ ์๋น์ค ์ฝ๋ ๋ณ๊ฒฝ 0์ค๋ก ์ ์ฅ์ ๊ต์ฒด | global/storage/FileStorage.java |
| ์ด๋ํฐ ํจํด | ์๊ทธ๋์ฒ๊ฐ ๋ค๋ฅธ ๊ธฐ์กด LocalFileStorage๋ฅผ ์ธํฐํ์ด์ค์ ๋ง์ถฐ ์ฌ์ฌ์ฉ |
LocalFileStorageAdapter.java |
| 2๋จ๊ณ ์ ์ฅ/์กฐํ | ์ ๋ก๋ ์ key๋ง DB ์ ์ฅ โ ์กฐํ ์์ ์ ์ ๊ทผ URL ์์ฑ | createPresignedGetUrl() |
| Presigned URL | ๋น๊ณต๊ฐ ๋ฒํท + ๋ง๋ฃ์๊ฐ ์๋ ์์ URL๋ง ๋ ธ์ถ | S3FileStorage.java:129-147 |
| ๋ก์ปฌ ์ธ๋ถ์์กด์ฑ ์ ๊ฑฐ | local ํ๋กํ์ S3 ๋น ์์ฒด๋ฅผ ๋์ฐ์ง ์์ | S3Config.java @Profile("!local") |
| ์์ ํ ๋ถ๊ฐ์์ | ๊ฐ์ฒด ์ญ์ ์คํจ ์ throw ๋์ log.warn โ ๋ฉ์ธ ํธ๋์ญ์
๋ณดํธ |
S3FileStorage.java:124-126 |
์ค๊ณ ์๋: ์ธ๋ถ ์คํ ๋ฆฌ์ง(S3)๋ฅผ ์ธํฐํ์ด์ค ๋ค๋ก ์จ๊ฒจ, ๋ก์ปฌ์์๋ ๋์คํฌยท์ด์์์๋ S3๋ฅผ ๋์ผํ ์ฝ๋๋ก ์ฌ์ฉํ๋ค. S3๋ ๋ฒํท์ ๋น๊ณต๊ฐ๋ก ๋๊ณ Presigned URL๋ก๋ง ์ ๊ทผ์ ํ์ฉํด ๋ ธ์ถ ๋ฒ์๋ฅผ ์ ํํ๋ค.
"๊ณตํต + ํ๋กํ๋ณ ์ค๋ฒ๋ผ์ด๋" 3๋จ ๊ตฌ์ฑ์ผ๋ก, ๋ก์ปฌ์ ์ง์ ์ฅ๋ฒฝ์ ๋ฎ์ถ๊ณ ์ด์์ ์ค์ ๋๋ฝ์ ์ฐจ๋จํฉ๋๋ค.
application.yml # ๊ณตํต๊ฐ + ๊ธฐ๋ณธ ํ๋กํ(default: local)
application-local.yml # ๋ก์ปฌ: DB ํ๋์ฝ๋ฉ + JWT secret ๊ธฐ๋ณธ๊ฐ ์ ๊ณต
application-railway.yml # ์ด์: DB/secret ์ ๋ถ ํ๊ฒฝ๋ณ์ ์ฃผ์
| ํ๊ฒฝ | DB ์ค์ | JWT Secret |
|---|---|---|
| local | localhost:3306 ํ๋์ฝ๋ฉ (docker-compose์ ์ง) |
๊ธฐ๋ณธ๊ฐ ์ ๊ณต โ ๋ฌด์ค์ ์ฆ์ ์คํ |
| railway | ${MYSQLHOST} ๋ฑ ํ๊ฒฝ๋ณ์ ์ฃผ์
|
ํ๊ฒฝ๋ณ์ ๊ฐ์ โ ๋ฏธ์ค์ ์ ๋ถํ ์คํจ |
์ค๊ณ ์๋: ๋ก์ปฌ์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ํด๋ก ํ ๋ฐ๋ก ์คํ ๊ฐ๋ฅํ๊ฒ, ์ด์์ ๋น๋ฐํค๋ฅผ ๊ฐ์ ํด ์ค์ ๋๋ฝ ์ ๋ถํ ์ด ์คํจํ๋๋ก ๋ถ๋ฆฌํ๋ค. DBยทJWTยทS3 ์๊ฒฉ์ฆ๋ช ์ ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ์๊ณ ํ๊ฒฝ๋ณ์๋ก ๋ถ๋ฆฌํด ์ฃผ์ ํ๋ค (์ด์: Railway ํ๊ฒฝ๋ณ์ / ๋ก์ปฌ: ์คํ ๊ตฌ์ฑ์ ํ๊ฒฝ๋ณ์).
Access(1์๊ฐ) + Refresh(7์ผ) ํ ํฐ ํ์ด ๊ตฌ์กฐ์ด๋ฉฐ, Refresh Token์ DB์ ์ ์ฅํด ์ฌ๋ฐ๊ธ ํ์ ๊ณผ ๋ก๊ทธ์์ ๋ฌดํจํ๋ฅผ ๊ตฌํํ์ต๋๋ค.
JwtTokenProviderโ HMAC-SHA256(Keys.hmacShaKeyFor, 256bit), userId/username/role์ claim์ผ๋ก ๋ฐ๊ธยท์ถ์ถRefreshToken์ํฐํฐ โ User์ 1:1, ์ฌ๋ฐ๊ธ ์updateToken()์ผ๋ก ํ์ , ๋ก๊ทธ์์ ์ DB์์ ์ญ์ ํด ์ฌ์ฌ์ฉ ์ฐจ๋จJwtAuthenticationFilter(OncePerRequestFilter) โ Authorization ํค๋์ Bearer ํ ํฐ ๊ฒ์ฆ ํSecurityContext์ค์ SessionCreationPolicy.STATELESSโ ์์ ๋ฌด์ํBCryptPasswordEncoderโ ๋น๋ฐ๋ฒํธ ๋จ๋ฐฉํฅ ํด์ฑ
@EnableMethodSecurity๋ฅผ ์ผ๊ณ ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ ๊ถํ์ ์ ์ธํด, URL ํจํด์ด ์๋ ๋น์ฆ๋์ค ๋ฉ์๋ ๋จ์๋ก ์ธ๊ฐ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.
@PreAuthorize("hasRole('MENTOR')") // ๊ณผ์ ๋ถ์ฌ, ํผ๋๋ฐฑ ์์ฑ
@PreAuthorize("hasRole('MENTEE')") // ํ๋๋/๊ณผ์ ์ ์ถ
@PreAuthorize("hasAnyRole('MENTOR','MENTEE')") // ๊ณตํต ์กฐํ์ญํ ์ UserRole enum(MENTOR/MENTEE)์ผ๋ก ๋ชจ๋ธ๋งํ๊ณ EnumType.STRING์ผ๋ก ์ ์ฅํฉ๋๋ค.
๋ชจ๋ ์๋ต์ CommonResponse<T> ํ ๊ฐ์ง ํฌ๋งท์ผ๋ก ํต์ผํ๊ณ , ์์ธ๋ฅผ ๋จ์ผ ํธ๋ค๋ฌ์์ ์ฒ๋ฆฌํฉ๋๋ค.
{ "isSuccess": true, "code": "COMMON_200", "message": "์ฑ๊ณต", "result": { } }CommonResponse<T>โ ์ ๋ค๋ฆญ ๋ํผ,@JsonInclude(NON_NULL)๋ก ์๋ฌ ์ result ์๋ตErrorStatusenum โ ๋๋ฉ์ธ๋ณ 40์ฌ ๊ฐ ์๋ฌ ์ฝ๋ (AUTH_4011,TASK_4001โฆ), HTTP ์ํ/์ฝ๋/๋ฉ์์ง ์ผ๊ด ์ ์GlobalExceptionHandler(@RestControllerAdvice) โ ๋น์ฆ๋์ค ์์ธ / ๊ฒ์ฆ ์คํจ(ํ๋๋ณ ์์ธ) / ํ์ผ ์ ๋ก๋ ์ด๊ณผ / ์ธ์ฆยท์ธ๊ฐ ์์ธ / ํด๋ฐฑ์ ํ ๊ณณ์์ ์ฒ๋ฆฌ
๊ฐ์น: ํ๋ก ํธ์๋๊ฐ ํญ์ ๊ฐ์ ๊ตฌ์กฐ๋ก ์๋ต์ ํ์ฑํ ์ ์๊ณ , ์๋ฌ ์ผ์ด์ค๊ฐ ์ฝ๋๋ก ํ์คํ๋์ด ํ์ ยท๋๋ฒ๊น ํจ์จ์ด ๋๋ค.
| ํฌ์ธํธ | ๊ทผ๊ฑฐ |
|---|---|
| N+1 ๋ฐฉ์ง | ์๋ณ ๊ณผ์ ์กฐํ์์ JOIN FETCH t.subject (TaskRepository) |
| ํธ๋์ญ์ ์ ๋ต | ๊ธฐ๋ณธ @Transactional(readOnly = true), ์ฐ๊ธฐ ๋ฉ์๋๋ง ์ฌ์ ์ |
| ์ง์ฐ ๋ก๋ฉ | ๋ชจ๋ ์ฐ๊ด๊ด๊ณ FetchType.LAZY |
| ์์ฑ์ ์ฃผ์ ์ผ๊ด | @RequiredArgsConstructor + final ํ๋ ์ ์ฉ (ํ๋ ์ฃผ์
๋ฏธ์ฌ์ฉ) |
| DTO ๋งคํ ์ปจ๋ฒค์ | ์ ์ ํฉํ ๋ฆฌ from() / of()๋ก ๋งคํ ๋ก์ง ์ค์ํ |
| ์ ๋ ฅ ๊ฒ์ฆ | Jakarta Validation(@NotBlank, @Size) + ๋น๋ฐ๋ฒํธ ํ์ธ ์ปค์คํ
๊ฒ์ฆ |
| ์ด๊ธฐ ๋ฐ์ดํฐ ์๋ฉ | SubjectInitializer(ApplicationRunner)๊ฐ ์ค๋ณต ์ฒดํฌ ํ ๊ณผ๋ชฉ ์๋ |
| ์์ฒญ ๋ก๊น | RequestLoggingFilter๋ก method/URI/status/์์์๊ฐ(ms) ๊ธฐ๋ก |
| ํ ์คํธ | Mockito BDD(given/when/then), ํ๊ธ @DisplayName, H2 ์ธ๋ฉ๋ชจ๋ฆฌ |
| API ๋ฌธ์ํ | Swagger(OpenAPI 3.0) + JWT Bearer SecurityScheme |
# 1) ๋ก์ปฌ MySQL ๊ธฐ๋ (port 3306, db=app / user=app / pw=app1234)
docker compose up -d
# 2) ์ ํ๋ฆฌ์ผ์ด์
์คํ (๊ธฐ๋ณธ ํ๋กํ: local)
./gradlew bootRun
# 3) ํ
์คํธ
./gradlew test