순환참조 없는 레이어드 아키텍처 + 공통 인프라를 제공하는 Spring Boot 백엔드 개발 표준화 프레임워크
실무에서 반복적으로 마주쳤던 두 가지 문제를 해결하기 위해 시작했습니다.
새 프로젝트가 시작될 때마다 이력 필드, API 응답 포맷, 검색 파라미터, 메서드 시그니처를 매번 다시 작성했습니다. 개발자마다 구현 방식이 달라 코드를 파악하는 데만 시간이 걸렸습니다.
// 팀마다 달랐던 API 응답 포맷
{code:200,data:{...}}
{status:"success",data:{...}}
// 팀마다 달랐던 메서드 시그니처
List<T> findAllBy(P p);
List<T> selectListBy(P p);
List<T> getListBy(P p);실무에서 두 가지 규칙을 모두 적용해봤는데, 둘 다 문제가 있었습니다.
| 규칙 | 문제 |
|---|---|
| 서비스 간 직접 호출 허용 | 프로젝트 규모가 커질수록 의존 관계가 얽혀 순환참조 에러 발생 |
| 서비스 간 직접 호출 금지 | 필요한 로직을 호출 못하니 같은 코드를 여러 곳에 중복 작성 |
→ Facade 레이어를 추가해 두 문제를 동시에 해결했습니다.
Service는 서로를 모르고, 로직 조합이 필요하면 Facade에서 여러 Service를 조합해 처리합니다.
Facade 패턴을 선택한 이유
- 서비스 간 순환 참조 방지
서비스 안에 다른서비스를 호출하는 걸 근본적으로 차단하기때문에, 순환 참조가 구조적으로 절대 발생할 수 없음
- Bean Validation 대신 ToyAssert를 Facade에서 처리와 검증
입력, 비즈니스 검증이 Facade 레이어 한 곳에 모여 있어, 유지보수가 용이함
flowchart LR
A[Controller] -->|의존| B[Facade] -->|의존| C[Service] -->|의존| D[Repository] -->|DB 접근| E[(Database)]
| 가능한것 | 안되는것 | |
|---|---|---|
| Controller | Facade 의존, 사용자 요청, 응답 반환 | 검증 로직, 직접 Service 호출 |
| Facade | Service 의존, 입력값 검증, 예외 처리, 트랜잭션, 비즈니스 로직 조합 | Repository 직접 접근, HTTP 관련 코드 |
| Service | Repository의존, 단일 도메인 비즈니스 로직 | Repository 직접 접근, 예외, HTTP 관련 코드, 다른 Service 의존 |
| Repository | DB 접근, Query | 비즈니스 로직 |
| 클래스 | 역할 |
|---|---|
| BaseService | 모든 Service가 구현하는 인터페이스. 동일한 메서드 시그니처 강제 |
| BaseEntity | PK 체계 단일화, 생성일/수정일 이력 필드 자동화 |
| BaseModel | Facade/Controller 계층에서 사용하는 DTO 기반 클래스 |
| BaseSearchParam | 검색 조건 공통 파라미터 규격화 |
- Entity (
extends BaseEntity<Long>): JPA 영속성 객체, DB와 직접 매핑하는 객체 - Model (
extends BaseModel<Long>): Facade/Controller 계층에서 주고받는 객체 - Converter (MapStruct): 양방향 변환 (
toModel/toEntity)
| 분류 | 기술 |
|---|---|
| 언어 / 플랫폼 | Java 21, Spring Boot 3.5, Gradle 8.14 |
| 데이터 접근 | Spring Data JPA, QueryDSL 5.0, Flyway |
| 매핑 | MapStruct 1.5.5, Lombok |
| 인증 / 보안 | Spring Security, JWT (jjwt 0.11.5) |
| 캐시 / 세션 | Redis 7 (토큰 저장소 + 캐시) |
| 데이터베이스 | PostgreSQL 15 |
| API 문서 | SpringDoc OpenAPI (Swagger UI) |
| 외부 통신 | Spring WebFlux (WebClient) |
| 로컬 인프라 | Docker Compose (PostgreSQL + Redis 자동 기동) |
| 모니터링 | Prometheus, Grafana, Actuator |
| 클라우드 | AWS EC2, RDS(PostgreSQL), ElastiCache(Redis) |
| 웹 서버 | Nginx (리버스 프록시) |
| CI/CD | GitHub Actions (main 브랜치 자동 배포) |
src/main/java/com/example/basicarch/
├── base/ 공통 인프라 (annotation, exception, model, redis, security, utils)
├── config/ Spring 설정 (Security, Redis, Swagger, advice, interceptor, scheduler)
└── module/
├── user/ 사용자, 역할, 인증
├── code/ 공통 코드/코드그룹
├── menu/ 메뉴 관리
└── file/ 파일 업로드/다운로드
각 모듈은 controller / facade / service / repository / converter / entity / model 구조를 따릅니다.
| 클래스 | 주요 기능 |
|---|---|
| StringUtils | isBlank/isEmpty, masking, lpad/rpad, regex, 포맷 |
| DateUtils | LocalDateTime-Date-String 변환, 요일, 날짜 연산 |
| CollectionUtils | safeStream, merge, separationList, toMap, extractList |
| CryptoUtils | 랜덤 문자열 생성, SecretKey 생성 |
| SessionUtils | SecurityContext에서 현재 사용자 정보 조회 |
| CommonUtils | HTTP 응답 직접 쓰기, JWT 디코딩 |
flowchart LR
A[클라이언트] -->|id + password| B[AuthFacade] --> C[UserService] --> D[JwtTokenService\n토큰 생성 + Redis 저장] -->|accessToken + refreshToken| A
flowchart LR
A[클라이언트] --> B[JwtTokenService]
B -->|만료 또는 변조| Z[❌ 인증 실패]
B -->|Redis 토큰과 불일치|Y[❌ 중복 로그인]
B -->|정상|C[새 accessToken 발급] --> A
RefreshTokenStore는 인터페이스로 추상화되어 있어 Redis 외 다른 저장소로 교체 가능합니다.
flowchart LR
A[클라이언트] -->|HTTP 요청| B[JwtAuthenticationFilter]
B -->|허용된 URL| E[요청 처리]
B -->|보호된 URL\n토큰 없음 또는 만료| Z[❌ 401]
B -->|보호된 URL\n토큰 유효| C[AuthUserDetailsService]
C --> D[SecurityContext 설정] --> E
flowchart LR
A[HTTP 요청] --> B[RoleInterceptor] --> C[MenuService]
C -->|미등록 URI/역할 포함| E[✅ 허용]
C -->|역할 미포함|F[❌ 차단]
| 캐시 | key:value | 갱신 주기 | 무효화 방식 |
|---|---|---|---|
| Code 캐시 | code::all | 스케줄러 N시간 | Redis Pub/Sub |
| Menu 캐시 | menu::all | 스케줄러 N시간 | Redis Pub/Sub |
| Refresh Token | {jwt:refresh:loginId : refreshToken} | JWT 만료 시간(TTL) | 로그아웃/재로그인 시 삭제 |
스케줄러 갱신 주기는 cron.yml에서 관리
module 디렉터리 범위 안의 모든 응답은 ResponseAdvice 가 Response로 자동 래핑합니다.
public static <T> Response<T> fail(int status, String error, String message) {
return Response.<T>builder()
.status(status)
.error(error)
.message(message)
.build();
}
public static <T> Response<T> fail(int status, String message) {
return Response.<T>builder()
.status(status)
.message(message)
.build();
}user - 사용자
Spring Security + JWT 기반 인증. Role / UserRole / User 구조로 권한을 관리하고, Redis에 Refresh Token을 저장해 체크합니다.
code - 공통 코드
CodeGroup → Code 1:N 계층으로 코드를 관리한다. code 값 없이 저장하면 자동으로 001, 002..., N 채번되고, 변경 시 리스너를 통한 캐시 무효화를 이용합니다.
menu - 메뉴
id와 parentId 를 이용한 트리 구조. DB에서 가져온 후 서버에서 트리로 조립합니다. 메뉴에 역할을
설정하면 RoleInterceptor로 접근을 제한합니다.
file - 파일
refPath(테이블명) + refId(PK) 조합으로 어느 도메인에나 파일을 붙일 수 있습니다.
요구 사항: JDK 21, Docker(Windows는 WSL2 필요)
| 프로필 | 포트 |
|---|---|
local |
8085 |
dev |
8080 |
prod |
8080 |
Docker가 설치되어 있으면 프로젝트만 구동하면 됩니다. 프로젝트 구동 시, LocalDockerConfig에서 프로젝트 인프라를 자동으로 세팅해줍니다.
- Swagger UI: http://localhost:8085/swagger-ui/index.html
- Prometheus: http://localhost:19090
- Grafana: http://localhost:13000
main 브랜치 push 시 GitHub Actions가 자동으로 빌드 후 EC2에 배포합니다.
main 브랜치 push 시 Railway에 자동으로 배포됩니다.
flowchart LR
A[Client] --> B[nginx:80] --> C[Spring Boot:8080]
C --> D[RDS - PostgreSQL]
C --> E[ElastiCache - Redis]
### B. Railway
2022년도에 JWT 개념을 처음 접했습니다. 당시 Refresh Token을 인메모리로 관리했는데, 인스턴스가 재시작되면 토큰이 날아가고 사용자가 강제 로그아웃되는 문제가 있었습니다. Basic-Arch에서는 처음부터 Redis에 저장하는 방식을 선택했기 때문에, 토큰관리를 좀더 용이하게 할수 있게 되었습니다.
Spring Cache를 사용하며, 두 가지 방식으로 캐시를 관리합니다.
docker exec -it basic-arch-redis redis-cli subscribe cache:invalidateRedis Pub/Sub — 데이터 변경 시 즉시 무효화
@CacheInvalidate(CacheType.CODE)
- 스케줄러 갱신 주기는 cron.yml에서 관리
| 순서 | 기술 | 학습 내용 | 상태 |
|---|---|---|---|
| 1 | Prometheus + Grafana | Actuator 메트릭 수집, 대시보드 구성 | 완료 |
| 2 | AWS + ElastiCache | EC2 배포, RDS(PostgreSQL), ElastiCache(Redis) 연동 | 진행중 |
| 3 | Kubernetes | minikube로 현재 프로젝트 배포, 및 실습 | 진행중 |
| 4 | Kafka | docker-compose에 Kafka 추가하고 Redis 데이터 유실 문제 처리 | 대기 |
| 5 | 명칭 변경 | Model → Dto, Converter → Mapper 명칭 변경(인텔리제이에서 지원하는 것 확인) |
대기 |




