html CSS 는 현재 피그마에맞게
- https://www.figma.com/community/file/1380235722331273046/simple-design-system
아키텍처 원칙
- Controller - 요청/응답 처리 , repository, entity 사용X
- Service - 비즈니스 로직 , Entity ↔ DTO 변환 담당. request,response 등 웹과 관련된 내용 X
- Repository - DB 데이터. Entity, DTO 둘다 반환 가능.
- 당연한거지만 Contorller에서 repository 호출 금지.
- DB조회나 해당 domain에 관한 로직은 service지만 파일이나 날짜 등과같은 공통기능은 Util로 이름짓기.
API 설계 규칙
- SSR에서는 Controller에서 그냥 aService.method1(),bService.method2() 호출. Facade 패턴은 필요할 때만. 근데 여기선 댓글과 파일은 그냥 CSR처럼 화면에서 따로 API 호출
- CSR은 1 API = 1 Controller 메소드 = 1 Service 메소드
프론트엔드에서 진행 흐름을 받고 API 여러번 호출하는 방식. 단 CSR도 API 여러번 호출이 부담되서 서버에서 한번에 처리해야된다면 이 때 Facade 패턴 적용해볼 것 - aService, bService등에서 사용하는 파라미터는 aService, bService에서만 사용하기 위한 작은 DTO로.
컨트롤러 (또는 facadeServie)에서 넘겨받은 DTO를 aService, bService에서 필요한 DTO로 변환해서 사용.
Entity와 DTO
- 변환메소드는 DTO 측에서, DTO → Entity : DTO에서 toEntity() 메소드로 변환 Entity → DTO : DTO에서 from(entity) 메소드로 변환 ModelMapper, MapStruct 등 변환해주는 lib 있음.
- Entity - DB 매핑, 비즈니스 편의 메소드 (상태 변경) 편의메소드에서 exception 발생시켜도 공통처리 됨.
-
DTO - 변환 메소드, API 요청/응답 될 수 있으면 매 요청,응답마다 DTO 분리 innerclasss는 공통필드 처리를 위한게 아니라 그냥 여러 java파일 개수 줄이는 것뿐
-
공통DTO 기준
- 중복코드 해결을 위해 공통DTO 만들 수 있지만 왠만하면 만들지말고 공통된 필드여도 다 DTO마다 넣는걸 권장.
- 필수여부 - 등록, 임시저장에서 공통으로 사용되는 필드들도 필수여부가 다르다면 공통DTO X
- 아무리 공통된 필드여도 요청용공통DTO, 응답용공통DTO는 분리하는걸 권장.
- 상속, compostiion으로 공통DTO 만들 수있지만 왠만하면 공통DTO 안 만드는 쪽으로.. DTO 상속은 오히려 헷갈리고 좋지않은 경우가 많음. composition을 쓰면 API 스펙이 변경됨. { commonData: {필드1, 필드2}}
로그인
User
- security 대비 입력은 username,password Long id는 JPA용. 실제 로그인id역할을 하는건 username
DTO 분리 (목적별)
원칙: 왠만하면 매 요청/응답마다 DTO 분리. 공통 DTO 지양.
| DTO | 역할 | 사용 위치 |
|---|---|---|
SessionUserDTO |
세션 저장용 사용자 정보 (id, username, nickname, email, roles) | CustomUserDetails 내부, 세션 |
| 로그인 요청 DTO | username, password (현재는 @RequestParam으로 처리) | Security가 자동 처리 |
| 회원가입 요청 DTO | username, password, nickname, email | UserController (현재는 @RequestParam) |
※ Security 적용 전에는
LoginUserDTO였으나, 목적이 “로그인”이 아니라 “세션 저장”이므로
SessionUserDTO로 명확히 분리. 이것이 “매 요청/응답마다 DTO 분리” 원칙.
Spring Security — 세션 방식 적용
- 의존성:
spring-boot-starter-security,thymeleaf-extras-springsecurity6 - 비밀번호: BCryptPasswordEncoder (단방향 해시, salt 내장)
개발자가 만드는 것 (2가지 + 설정)
| 파일 | 역할 |
|——|——|
| SecurityConfig | URL 접근 권한, 로그인/로그아웃 URL, PasswordEncoder Bean 등록 |
| CustomUserDetails | UserDetails 구현 — SessionUserDTO 재사용 (composition) + password 추가 |
| CustomUserDetailsService | UserDetailsService 구현 — username으로 DB 조회 후 UserDetails 반환 |
Entity → DTO 변환 원칙 준수:
CustomUserDetails는 필드를 직접 선언하지 않고SessionUserDTO를 composition으로 갖는다.
CustomUserDetails.from(entity)→ 내부에서SessionUserDTO.from(entity)재사용 + password 추가.
Controller에서 Entity 사용 금지 원칙 준수.
로그인 흐름 (Security가 자동 처리)
사용자가 username/password 입력 → /loginProc POST
↓ Security가 요청 가로챔 (UsernamePasswordAuthenticationFilter)
↓ CustomUserDetailsService.loadUserByUsername(username) 자동 호출
↓ DB에서 사용자 조회 → CustomUserDetails로 감싸서 반환
↓ Security가 BCrypt로 password 비교
↓ 성공 → Authentication 생성 → 세션에 저장 / 실패 → /login?error 리다이렉트
- POST /login(로그인 처리), POST /logout(로그아웃)은 Controller 없음 — Security가 처리
- GET /login(로그인 페이지)만 Controller 필요
SecurityConfig URL 접근 규칙
permitAll() → "/", "/login", "/signup" (비로그인 허용)
hasAuthority("ADMIN") → "/admin" (ADMIN만)
hasAnyAuthority() → "/mypage" (ADMIN 또는 USER)
anyRequest() → permitAll() (나머지 허용)
Controller에서 로그인 정보 사용
// @AuthenticationPrincipal로 CustomUserDetails 주입 → model에 담아서 HTML에서 사용
@GetMapping("/mypage")
public String mypage(@AuthenticationPrincipal CustomUserDetails userDetails, Model model) {
model.addAttribute("user", userDetails); // id, username, nickname, email, role 사용 가능
}
Thymeleaf에서 사용
- 로그인 여부 분기:
sec:authorize사용 (index.html 헤더 등) - 사용자 정보 표시: Controller에서 model에 담은
${user}사용 (기존 Thymeleaf와 동일)
<!-- 로그인/비로그인 분기 — sec 태그 사용 -->
<div sec:authorize="isAuthenticated()">로그인 상태</div>
<div sec:authorize="isAnonymous()">비로그인 상태</div>
<div sec:authorize="hasAuthority('ADMIN')">관리자 전용</div>
<!-- 사용자 정보 표시 — model에 담은 CustomUserDetails(user)를 일반 EL로 사용 -->
<span th:text="${user.nickname}">닉네임</span> <!-- getNickname() -->
<span th:text="${user.username}">아이디</span> <!-- getUsername() -->
<span th:text="${user.email}">이메일</span> <!-- getEmail() -->
<span th:text="${user.roles}">USER</span> <!-- getRoles() -->
sec:authentication="principal.nickname"방식도 가능하지만,
model에 담아서${user.nickname}으로 쓰는 것이 기존 Thymeleaf와 같아서 직관적.
이 프로젝트에서는 model 방식을 사용한다.
UserEntity 변경사항
roles필드 추가 (String: 쉼표 구분 — “USER”, “ADMIN,USER”)updatePassword()메서드 추가 (BCrypt 인코딩용)- password 컬럼: BCrypt 해시값 저장 (60자)
초기 데이터 (DataInitializer)
- data-users.sql에서 평문 비밀번호로 INSERT
- DataInitializer(CommandLineRunner)가 서버 시작 시 BCrypt로 자동 인코딩
- 회원가입 시에는 UserService에서 BCrypt 인코딩 후 저장
테스트 계정
| username | password | role | 비고 | |———-|———-|——|——| | admin | test1234 | ADMIN,USER | 관리자 — /admin 접근 가능 | | user1~15 | test1234 | USER | 일반 사용자 |
🗄️ JPA 설계
연관관계 원칙
- 양방향 지양 : Entity에서 지양하는거지 DTO는 상관없음.
- 글 상세보기 화면
글1 -댓글N (파일도 마찬가지 )- Service에서 글 조회, 댓글 조회 후 응답DTO에 세팅
- service에서 글+조회 조회 후 응답DTO에 세팅
- 글 조회 후 프론트에서 댓글 조회 API (여기서는 이걸 선택) 여기는 어쨋든 부모글 한번, 댓글List 한번 호출 = 총2번 호출
- 메인 데이터 여러개 연관관계 조회 (댓글목록 한번에 조회 + 각 댓글에 user username이 필요한상황)
방법 1. 메인 쿼리 실행 후 서브쿼리id 모아서
IN쿼리로 한번에 처리 (여기서는 이거) 방법 2. 조인 - jpql, @EntityGraph 등으로 한번에 쿼리 실행 상황에 맞게 선택. 이후 성능 문제가 있다면 개선. 방법1,2 둘다 repository(queryDSL)에서 처리.
연관관계가 너무 많을 때만 service에서 처리하는거 고려.