๐Ÿ” 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 ๋“ฑ ์ด์ „ ํ”„๋กœ์ ํŠธ ์ž”์žฌ๊ฐ€ ๋‚จ์•„์žˆ์Œ.

ํ•ด๋‹น ํŒŒ์ผ๋“ค:


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 ํ˜ผ์žฌ ๐ŸŸข ์„ ํƒ ํŒŒ์ผ ๋ถ„๋ฆฌ