๐ Spring CSR ๊ฐ์์ฉ ํ๋ก์ ํธ โ ์ ์ฒด ์ฝ๋ ๋ฆฌ๋ทฐ ๊ฒฐ๊ณผ
๋ฆฌ๋ทฐ ์ผ์: 2026-04-01
๋์: Security / JWT / OAuth2 / ๊ฒ์ํ(Community) / ๋๊ธ(Comment) / ํ์ผ(File) / ์ฑํ (STOMP) ์ ์์ญ
โ ์ดํ
๊ฐ์์ฉ ํ๋ก์ ํธ๋ก์ ๊ตฌ์กฐ, ํ๋ฆ, ์ฃผ์ ์ค๋ช
๋ชจ๋ ์ฐ์ํฉ๋๋ค.
JWT + OAuth2 + STOMP๊น์ง CSR ๋ฐฉ์์ผ๋ก ์ผ๊ด๋๊ฒ ๊ตฌํ๋์ด ์๊ณ ,
ํจํค์ง ๋ถ๋ฆฌ(controller/service/repository/entity/model/handler/filter)๋ ๊น๋ํฉ๋๋ค.
์๋๋ ์ค์ ๋์์ ์ํฅ์ ์ค ์ ์๋ ๋ฒ๊ทธ/๋ฌธ์ ์ ๊ฐ์ ํ์ง ํฅ์์ ์ํ ๊ฐ์ ์ฌํญ์ ๊ตฌ๋ถํ์ฌ ์ ๋ฆฌํ ๊ฒ์ ๋๋ค.
๐ด [์ค์ ๋ฒ๊ทธ / ๋์ ์ค๋ฅ] โ ์์ ๊ถ์ฅ
1. AppOAuth2Controller โ Refresh ํ ํฐ DB ๋ฏธ์ ์ฅ
ํ์ผ: jwt/controller/AppOAuth2Controller.java (line ~98)
// ํ์ฌ ์ฝ๋: refreshToken์ ์์ฑ๋ง ํ๊ณ DB์ ์ ์ฅํ์ง ์์
String accessToken = jwtUtil.createAccessToken(username);
String refreshToken = jwtUtil.createRefreshToken(username);
// โ refreshService.saveRefresh(refreshToken) ํธ์ถ์ด ์์!
๋ฌธ์ : ์ฑ์์ OAuth2 ๋ก๊ทธ์ธ ํ ๋ฐ์ refreshToken์ผ๋ก /api/tokens/refresh ํธ์ถ ์,
DB์ ํ ํฐ์ด ์์ผ๋ฏ๋ก โํ๊ธฐ๋ ํ ํฐโ ์๋ฌ ๋ฐ์ โ ํ ํฐ ์ฌ๋ฐ๊ธ ๋ถ๊ฐ
์์ : refreshService.saveRefresh(refreshToken); ์ถ๊ฐ ํ์
2. RefreshController โ ์ฟ ํค secure ํ๋์ฝ๋ฉ
ํ์ผ: jwt/controller/RefreshController.java (line ~113)
private void addCookie(HttpServletResponse response, String name, String value) {
ResponseCookie cookie = ResponseCookie.from(name, value)
.httpOnly(true)
.secure(false) // โ ํ๋์ฝ๋ฉ! TODO ์ฃผ์๋ง ์๊ณ ์ค์ ๋ฏธ์ ์ฉ
.sameSite("Lax")
.path("/")
.build();
๋ฌธ์ : JwtLoginFilter์ OAuth2LoginSuccessHandler๋ @Value("${app.cookie.secure}") ๋ก ํ๊ฒฝ๋ณ ๋ถ๊ธฐํ๋๋ฐ,
RefreshController๋ง false ํ๋์ฝ๋ฉ. ์ด์(HTTPS) ๋ฐฐํฌ ์ ์ฟ ํค ๋ณด์ ๋ถ์ผ์น.
์์ : @Value("${app.cookie.secure:false}") private boolean secureCookie; ์ฃผ์
ํ ์ฌ์ฉ
3. data-init.sql โ ์กด์ฌํ์ง ์๋ ํ
์ด๋ธ ์ฐธ์กฐ
ํ์ผ: resources/data-init.sql
DELETE FROM reviews; -- โ reviews ํ
์ด๋ธ/์ํฐํฐ ์์
DELETE FROM doll_shop; -- โ doll_shop ํ
์ด๋ธ/์ํฐํฐ ์์
๋ฌธ์ : ์ด์ ํ๋ก์ ํธ์์ ๋จ์ ์์ฌ. data-init.sql์ ํ์ฌ application.yml์ data-locations์ ํฌํจ๋์ด ์์ง ์์
๋ก์ปฌ์์๋ ์คํ๋์ง ์์ง๋ง, ์ด์ ํ๊ฒฝ์์ sql.init.mode: never์ด๋ฏ๋ก ์ญ์ ์คํ ์ ๋จ.
๋ค๋ง ํผ๋์ ์ค ์ ์์ผ๋ฏ๋ก ์ ๋ฆฌ ๊ถ์ฅ.
4. test/resources/application.yml โ ์กด์ฌํ์ง ์๋ SQL ํ์ผ ์ฐธ์กฐ
ํ์ผ: src/test/resources/application.yml
data-locations:
- classpath:data-users.sql
- classpath:data-dollshop.sql # โ ํ์ผ ์์ โ ํ
์คํธ ์ ์ค๋ฅ
- classpath:data-files.sql
- classpath:data-review.sql # โ ํ์ผ ์์ โ ํ
์คํธ ์ ์ค๋ฅ
๋ฌธ์ : data-dollshop.sql, data-review.sql ํ์ผ์ด ์กด์ฌํ์ง ์์ ํ
์คํธ ์คํ ์ ์ค๋ฅ ๋ฐ์.
๋ํ data-community.sql, data-comment.sql, data-rooms.sql์ด ๋๋ฝ๋์ด ์์.
์์ : ๋ฉ์ธ application.yml๊ณผ ๋์ผํ๊ฒ ๋ง์ถ๊ธฐ:
data-locations:
- classpath:data-users.sql
- classpath:data-files.sql
- classpath:data-community.sql
- classpath:data-comment.sql
- classpath:data-rooms.sql
5. UserEntity.roles โ List<String> JPA ๋งคํ ๋ฌธ์
ํ์ผ: jwt/entity/UserEntity.java
@Builder.Default
private List<String> roles = new ArrayList<>();
๋ฌธ์ : @ElementCollection์ด๋ @Convert ์ด๋
ธํ
์ด์
์์ด List<String>์ ํ๋๋ก ์ฌ์ฉ.
JPA ๊ธฐ๋ณธ ์ง๋ ฌํ(Java Serialization โ BLOB)์ ์์กดํ๊ณ ์์ผ๋ฉฐ, ์ค์ SQL์์๋ 0xACED... ๋ฐ์ด๋๋ฆฌ๋ก ์ ์ฅ๋จ.
๋์์ ํ์ง๋ง, ๊ฐ์์์ ์ค๋ช
์ โ์ ์ด๋ ๊ฒ ์ ์ฅ๋๋์งโ ํผ๋์ ์ค ์ ์๊ณ , DB์์ ์ง์ ์กฐํ/์์ ์ด ๋ถ๊ฐ.
๊ถ์ฅ ๊ฐ์ (์ ํ):
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
@Builder.Default
private List<String> roles = new ArrayList<>();
๋จ, ์ด ๊ฒฝ์ฐ data-users.sql๋ ๋ณ๊ฒฝ ํ์. ํ์ฌ ๋ฐฉ์์ด ์๋์ ์ด๋ผ๋ฉด ์ฃผ์์ผ๋ก ์ค๋ช ์ถ๊ฐ ๊ถ์ฅ.
๐ก [์ ์ฌ์ ๋ฌธ์ / ๊ฐ์ ํ์ง ๊ฐ์ ] โ ๊ฐ์ ๊ถ์ฅ
6. QuerydslConfig ์์ SwaggerConfig ๋ด๋ถ ํด๋์ค
ํ์ผ: common/config/QuerydslConfig.java
Querydsl ์ค์ ํด๋์ค ์์ Swagger ์ค์ ์ด static class SwaggerConfig๋ก ํฌํจ๋จ.
๋์ ๋ฌธ์ ๋ ์์ง๋ง, ๊ฐ์ ์ โ์ Swagger ์ค์ ์ด Querydsl ํ์ผ์?โ๋ผ๋ ์ง๋ฌธ์ด ๋์ฌ ์ ์์.
๋ณ๋ ํ์ผ๋ก ๋ถ๋ฆฌํ๊ฑฐ๋, ํด๋์ค๋ช
์ AppConfig๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ ๊ถ์ฅ.
7. Swagger ์ค๋ช ๋ด์ฉ ๋ถ์ผ์น
ํ์ผ: common/config/QuerydslConfig.java (line ~48)
.info(new Info()
.title("์ธํ๋ฝ๊ธฐ๋ฐฉ API") // โ ํ์ฌ ํ๋ก์ ํธ์ ๋ง์ง ์์
.description("์ธํ๋ฝ๊ธฐ๋ฐฉ ์๋น์ค REST API ๋ฌธ์")
FileEntity.RefType์ Javadoc์๋ DOLL_SHOP, REVIEW, DOLL ๋ฑ ์ด์ ํ๋ก์ ํธ ์์ฌ๊ฐ ๋จ์์์.
ํด๋น ํ์ผ๋ค:
FileService.javaโ Javadoc์DOLL_SHOP, COMMUNITY, REVIEW, DOLLFileController.javaโ Javadoc์DOLL_SHOP, COMMUNITY, REVIEW, DOLLFileEntity.RefTypeenum โ ์ค์ ๋ก๋COMMUNITY,USER๋ง ์กด์ฌ (์ ์)
8. CommentService.createComment โ ์ญ์ ๋ ๊ฒ์๊ธ์๋ ๋๊ธ ์์ฑ ๊ฐ๋ฅ
ํ์ผ: community/comment/CommentService.java (line ~44)
CommunityEntity community = communityRepository.findById(createDTO.getCommunityId())
// โ findByIdAndIsDeletedFalse()๊ฐ ์๋ findById() ์ฌ์ฉ
๋ฌธ์ : ์ํํธ ์ญ์ ๋ ๊ฒ์๊ธ์๋ ๋๊ธ ์์ฑ์ด ๊ฐ๋ฅํจ.
์์ :
communityRepository.findByIdAndIsDeletedFalse(createDTO.getCommunityId())
9. SecurityConfig โ authorizeHttpRequests ๋ ๋ฒ ํธ์ถ
ํ์ผ: jwt/config/SecurityConfig.java (line 56, 79)
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/h2-console/**").permitAll() // ์ฒซ ๋ฒ์งธ ํธ์ถ
);
// ...
http.authorizeHttpRequests(auth -> auth // ๋ ๋ฒ์งธ ํธ์ถ
.requestMatchers("/css/**", ....).permitAll()
๋ฌธ์ : authorizeHttpRequests()๋ฅผ ๋ ๋ฒ ํธ์ถํ๋ฉด ๋ง์ง๋ง ์ค์ ๋ง ์ ์ฉ๋จ.
์ฒซ ๋ฒ์งธ H2 ์ฝ์ ์ค์ ์ด ๋ ๋ฒ์งธ์์ ๋ฎ์ด์จ์ ธ ๋ฌด์๋ ์ ์์.
(์ค์ ๋ก ๋ ๋ฒ์งธ ๋ธ๋ก์ /h2-console/**์ด ๋ค์ ํฌํจ๋์ด ์์ด ๋์์ ํ์ง๋ง, ์ค๋ช
์ ํผ๋ ๊ฐ๋ฅ)
๊ถ์ฅ: ํ๋์ authorizeHttpRequests() ๋ธ๋ก์ผ๋ก ํตํฉํ๊ฑฐ๋,
H2 ๊ด๋ จ ์ค์ ์ ๋ ๋ฒ์งธ ๋ธ๋ก์๋ง ๋จ๊ธฐ๊ธฐ.
10. DemoApplication โ ํ๊ฒฝ๋ณ์ ์ฝ์ ์ถ๋ ฅ
ํ์ผ: DemoApplication.java
System.out.println("KAKAO_CLIENT_ID: " + context.getEnvironment().getProperty("KAKAO_CLIENT_ID"));
System.out.println("GOOGLE_CLIENT_ID: " + context.getEnvironment().getProperty("GOOGLE_CLIENT_ID"));
๋ฌธ์ : ์ด์ ํ๊ฒฝ์์ ๋ฏผ๊ฐ ์ ๋ณด(Client ID)๊ฐ ์ฝ์/๋ก๊ทธ์ ๋
ธ์ถ๋จ.
๊ฐ์์ฉ์ด๋ผ ํฐ ๋ฌธ์ ๋ ์๋์ง๋ง, ํ์๋ค์ด ๊ทธ๋๋ก ๋ฐฐํฌํ ์ ์์ผ๋ฏ๋ก ์ฃผ์ ๊ฒฝ๊ณ ๊ถ์ฅ.
๐ข [์ ๋ ๋ถ๋ถ] โ ๊ฐ์์ฉ์ผ๋ก ํ๋ฅญํ ๊ตฌํ
| ์์ญ | ํ๊ฐ |
|---|---|
| JWT ํ ํฐ ๊ตฌ์กฐ | Access/Refresh ๋ถ๋ฆฌ, token_type ํด๋ ์ ํ์ฉ, ๋ง๋ฃ ์ ๋ช ํํ ์๋ฌ ์ฝ๋ โ ์ฐ์ |
| ํํฐ ์ฒด์ธ | JwtLoginFilter โ JwtAccessTokenCheckAndSaveUserInfoFilter ์์ ์ ์ , ์ฃผ์ ์ค๋ช
์์ธ |
| OAuth2 ํตํฉ | CustomUserAccount๊ฐ UserDetails + OAuth2User ๋์ ๊ตฌํ, OAuthProvider enum ํจํด โ ๊น๋ |
| InMemoryAuthorizationRequestRepository | STATELESS ํ๊ฒฝ์์ session ๋์ ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ, ์ค์ผ์ค๋ฌ๋ก 5๋ถ ํ ์๋ ์ญ์ โ ์ ์ค๊ณ๋จ |
| ์น/์ฑ ๋ถ๊ธฐ | ๋ก๊ทธ์ธ, ๋ก๊ทธ์์, ํ ํฐ ์ฌ๋ฐ๊ธ ๋ชจ๋ Accept ํค๋ ๋๋ ํ ํฐ ์์น๋ก ๋ถ๊ธฐ โ ์ผ๊ด์ฑ ์์ |
| ๊ฒ์ํ CRUD | Soft Delete, ์์ฑ์ ๊ฒ์ฆ, QueryDSL ๋์ ๊ฒ์, ํ์ด์ง โ ํ์ค์ ๊ตฌํ |
| ๋๊ธ | N+1 ๋ฐฉ์ง(fetch join), ์นด์ดํธ ์ฟผ๋ฆฌ ์ต์ ํ(IN ์ฟผ๋ฆฌ) โ ์ฑ๋ฅ ๊ณ ๋ ค๋จ |
| ํ์ผ ์์คํ | Strategy ํจํด(Local/Supabase), @ConditionalOnProperty ํ์ฉ โ ์ค๊ณ ํจํด ํ์ต์ ์ข์ |
| STOMP ์ฑํ | HTTP ํธ๋์ ฐ์ดํฌ์์ ์ฟ ํค JWT ์ถ์ถ โ STOMP CONNECT์์ ์ต์ข ์ธ์ฆ โ 2๋จ๊ณ ์ธ์ฆ ์ ๊ตฌํ |
| ์์ธ ์ฒ๋ฆฌ | BusinessException ๊ณ์ธต ๊ตฌ์กฐ, GlobalExceptionHandler, ErrorResponse ํต์ผ โ ์ฐ์ |
| API ์๋ต ํต์ผ | ApiResponse<T>, PageResponse<T>, ErrorResponse ๊ตฌ์กฐ ์ผ๊ด์ฑ โ ๊ฐ์์ฉ์ผ๋ก ์ ํฉ |
๐ ์์ ์ฐ์ ์์ ์์ฝ
| ์์ | ํญ๋ชฉ | ์ฌ๊ฐ๋ | ๋์ด๋ |
|---|---|---|---|
| 1 | AppOAuth2Controller refreshToken DB ๋ฏธ์ ์ฅ | ๐ด ๋ฒ๊ทธ | 1์ค ์ถ๊ฐ |
| 2 | test/application.yml ์กด์ฌํ์ง ์๋ SQL ์ฐธ์กฐ | ๐ด ํ ์คํธ ๊นจ์ง | ์ค์ ์์ |
| 3 | RefreshController ์ฟ ํค secure ํ๋์ฝ๋ฉ | ๐ด ์ด์ ์ ๋ฌธ์ | 2์ค ์์ |
| 4 | CommentService ์ญ์ ๋ ๊ฒ์๊ธ ๋๊ธ ํ์ฉ | ๐ก ๋ก์ง ๊ฒฐํจ | 1์ค ์์ |
| 5 | data-init.sql ์์ฌ ํ ์ด๋ธ ์ฐธ์กฐ | ๐ก ํผ๋ ์ ๋ฐ | 2์ค ์ญ์ |
| 6 | Swagger/Javadoc ์ด์ ํ๋ก์ ํธ ์์ฌ | ๐ก ๊ฐ์ ํ์ง | ํ ์คํธ ์์ |
| 7 | SecurityConfig authorizeHttpRequests ์ค๋ณต | ๐ก ์ค๋ช ํผ๋ | ๊ตฌ์กฐ ์ ๋ฆฌ |
| 8 | QuerydslConfig + SwaggerConfig ํผ์ฌ | ๐ข ์ ํ | ํ์ผ ๋ถ๋ฆฌ |