Skip to content

sseuldev/spring-instagram-20th

ย 
ย 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

62 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

spring-instagram-20th

CEOS 20th BE study - instagram clone coding


1-1. ๋„๋ฉ”์ธ ๋ฐ ERD ๋ถ„์„

img.png

  • ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ
  • ๊ฒŒ์‹œ๊ธ€์— ์‚ฌ์ง„๊ณผ ํ•จ๊ป˜ ๊ธ€ ์ž‘์„ฑํ•˜๊ธฐ
  • ๊ฒŒ์‹œ๊ธ€์— ๋Œ“๊ธ€ ๋ฐ ๋Œ€๋Œ“๊ธ€ ๊ธฐ๋Šฅ
  • ๊ฒŒ์‹œ๊ธ€์— ์ข‹์•„์š” ๊ธฐ๋Šฅ
  • ๊ฒŒ์‹œ๊ธ€, ๋Œ“๊ธ€, ์ข‹์•„์š” ์‚ญ์ œ ๊ธฐ๋Šฅ
  • ์œ ์ € ๊ฐ„ 1:1 DM ๊ธฐ๋Šฅ

์ด 6๊ฐ€์ง€ ๊ธฐ๋Šฅ์— ๋งž์ถ”์–ด ์ธ์Šคํƒ€๊ทธ๋žจ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๋ง์„ ์ง„ํ–‰ํ•ด๋ณด์•˜๋‹ค.

๋„๋ฉ”์ธ์€ ์ด 5๊ฐœ (/member, /post, /comment, /hashtag, /chat) ๋กœ ๊ตฌ์„ฑํ•˜์˜€๋‹ค.

1. member

ํšŒ์›๊ฐ€์ž…

  • ๊ฐ€์ž…์„ ์œ„ํ•ด์„œ๋Š” ํœด๋Œ€ํฐ ๋˜๋Š” ์ด๋ฉ”์ผ ์ธ์ฆ์„ ๊ฑฐ์ณ์•ผํ•œ๋‹ค.
  • ์ธ์ฆ ํ›„์—๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฆ„, ์‚ฌ์šฉ์ž์ด๋ฆ„(๋‹‰๋„ค์ž„)์„ ๊ธฐ์ž…ํ•œ๋‹ค.

๋งˆ์ดํŽ˜์ด์ง€

  • ๋งˆ์ดํŽ˜์ด์ง€์—๋Š” ํ”„๋กœํ•„์ด๋ฏธ์ง€, ์ด๋ฆ„, ์‚ฌ์šฉ์ž์ด๋ฆ„, ์†Œ๊ฐœ, ๋งํฌ, ์ž‘์„ฑํ•œ ๊ฒŒ์‹œ๊ธ€ ๋“ฑ์ด ํฌํ•จ๋œ๋‹ค.

์ƒํƒœ

  • ๋ชจ๋“  ํšŒ์›์˜ ์ƒํƒœ๋Š” active, inactive ์ค‘ ํ•˜๋‚˜์ด๋‹ค.
  • ํšŒ์› ํƒˆํ‡ด ์‹œ, inactive ์ƒํƒœ๋กœ ๋‘๊ณ  ์ผ์ • ๊ธฐ๊ฐ„๋™์•ˆ ๋น„ํ™œ์„ฑ์ธ ๊ฒฝ์šฐ ์ž๋™ ํƒˆํ‡ด ์ฒ˜๋ฆฌ๊ฐ€ ์ด๋ฃจ์–ด์ง„๋‹ค.

2. post

๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ

  • ๊ฒŒ์‹œ๊ธ€์—๋Š” ๋‚ด์šฉ, ์œ„์น˜, ์ด๋ฏธ์ง€(์ตœ๋Œ€ 10์žฅ), ์Œ์•… ๋“ฑ์„ ๋„ฃ์„ ์ˆ˜ ์žˆ๋‹ค.

๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ

  • ๊ฒŒ์‹œ๊ธ€์€ ํ”„๋กœํ•„ ์‚ฌ์ง„, ์‚ฌ์šฉ์ž ์ด๋ฆ„, ์ด๋ฏธ์ง€, ๋Œ“๊ธ€ ๊ฐœ์ˆ˜, ์œ„์น˜, ์Œ์•…, ํ•ด์‹œํƒœ๊ทธ ๋“ฑ์„ ํฌํ•จํ•œ๋‹ค.

3. comment

๋Œ“๊ธ€

  • ๋Œ“๊ธ€์—๋Š” ๋‚ด์šฉ, ์ข‹์•„์š” ๊ฐœ์ˆ˜ ๋“ฑ์ด ํฌํ•จ๋œ๋‹ค.
  • ๋Œ“๊ธ€์—๋Š” ๋ถ€๋ชจ ๋Œ“๊ธ€์„ ๊ธฐ์ค€์œผ๋กœ ๋Œ€๋Œ“๊ธ€์ด ๋‹ฌ๋ฆด ์ˆ˜ ์žˆ๋‹ค.
  • ๋Œ€๋Œ“๊ธ€์€ ์ž์‹ ์ด ๋‹ฌ๋ฆฐ ๋ถ€๋ชจ ๋Œ“๊ธ€์˜ ์ฐธ์กฐ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.
  • ๋ชจ๋“  ๋Œ“๊ธ€์—๋Š” ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ ์ˆ˜ ์žˆ๋‹ค.

4. hashtag


5. chat

์ฑ„ํŒ…๋ฐฉ(๋””์—  ๋ฆฌ์ŠคํŠธ)

  • ์‚ฌ์šฉ์ž์™€ ๋˜ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ๊ฐ„์˜ ๋””์— ์„ ์ฃผ๊ณ ๋ฐ›๋Š” ๊ณต๊ฐ„์ด๋‹ค.

๋ฉ”์„ธ์ง€(๋””์— )

  • ์‚ฌ์šฉ์ž ๊ฐ„ ์ผ๋Œ€์ผ๋กœ ์ง„ํ–‰๋˜๋ฉฐ, ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ด๋ฃจ์–ด์ง„๋‹ค.

โœ”๏ธŽ ์—ฐ๊ด€๊ด€๊ณ„ ๋ถ„์„

  1. ํšŒ์› - ๊ฒŒ์‹œ๊ธ€, ํšŒ์› - ๋ฉ”์„ธ์ง€, ํšŒ์› - ์ฑ„ํŒ…๋ฐฉ, ํšŒ์› - ๋Œ“๊ธ€์ข‹์•„์š”, ํšŒ์› - ๊ฒŒ์‹œ๊ธ€์ข‹์•„์š”, ํšŒ์› - ๋Œ“๊ธ€ (1:N ์ผ๋Œ€๋‹ค๊ด€๊ณ„)

    : ํ•œ ๋ช…์˜ ํšŒ์›์€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ฒŒ์‹œ๊ธ€ / ๋ฉ”์„ธ์ง€ / ์ฑ„ํŒ…๋ฐฉ / ๋Œ“๊ธ€์ข‹์•„์š” / ๊ฒŒ์‹œ๊ธ€์ข‹์•„์š” / ๋Œ“๊ธ€์„ ๊ฐ€์ง€์ง€๋งŒ, ์ด๋“ค์€ ํ•œ ๋ช…์˜ ํšŒ์›์—๋งŒ ์†ํ•œ๋‹ค.

  2. ์ฑ„ํŒ…๋ฐฉ - ๋ฉ”์„ธ์ง€ (1:N ์ผ๋Œ€๋‹ค๊ด€๊ณ„)

    : ํ•˜๋‚˜์˜ ์ฑ„ํŒ…๋ฐฉ์€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ฉ”์„ธ์ง€๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์˜ ๋ฉ”์„ธ์ง€๋Š” ํ•˜๋‚˜์˜ ์ฑ„ํŒ…๋ฐฉ์—๋งŒ ์†ํ•œ๋‹ค.

  3. ๊ฒŒ์‹œ๊ธ€ - ๋Œ“๊ธ€, ๊ฒŒ์‹œ๊ธ€ - ๊ฒŒ์‹œ๊ธ€์ข‹์•„์š”, ๊ฒŒ์‹œ๊ธ€ - ์ด๋ฏธ์ง€ (1:N ์ผ๋Œ€๋‹ค๊ด€๊ณ„)

    : ํ•˜๋‚˜์˜ ๊ฒŒ์‹œ๊ธ€์€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋Œ“๊ธ€ / ๊ฒŒ์‹œ๊ธ€์ข‹์•„์š” / ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ง€์ง€๋งŒ, ์ด๋“ค์€ ํ•˜๋‚˜์˜ ๊ฒŒ์‹œ๊ธ€์—๋งŒ ์†ํ•œ๋‹ค.

  4. ๊ฒŒ์‹œ๊ธ€ - ๊ฒŒ์‹œ๊ธ€ํ•ด์‹œํƒœ๊ทธ, ํ•ด์‹œํƒœ๊ทธ - ๊ฒŒ์‹œ๊ธ€ํ•ด์‹œํƒœ๊ทธ (1:N ์ผ๋Œ€๋‹ค๊ด€๊ณ„)

    • ๊ฒŒ์‹œ๊ธ€ - ํ•ด์‹œํƒœ๊ทธ ๊ฐ„ ์—ฐ๊ฒฐ์„ ์œ„ํ•ด ์ค‘๊ฐ„ ์—”ํ‹ฐํ‹ฐ์ธ ๊ฒŒ์‹œ๊ธ€ํ•ด์‹œํƒœ๊ทธ๋ฅผ ํ™œ์šฉํ•˜์˜€๋‹ค.

    • ํ•˜๋‚˜์˜ ๊ฒŒ์‹œ๊ธ€์€ ์—ฌ๋Ÿฌ ๊ฒŒ์‹œ๊ธ€ํ•ด์‹œํƒœ๊ทธ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๊ณ , ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํ•ด์‹œํƒœ๊ทธ๋„ ์—ฌ๋Ÿฌ ๊ฒŒ์‹œ๊ธ€ํ•ด์‹œํƒœ๊ทธ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค. ์ด๋•Œ ๊ฒŒ์‹œ๊ธ€ํ•ด์‹œํƒœ๊ทธ๋Š” ํ•˜๋‚˜์˜ ๊ฒŒ์‹œ๊ธ€๊ณผ ํ•ด์‹œํƒœ๊ทธ์—๋งŒ ์ข…์†๋œ๋‹ค.

  5. ๋Œ“๊ธ€ - ๋Œ“๊ธ€์ข‹์•„์š” (1:N ์ผ๋Œ€๋‹ค๊ด€๊ณ„)

    : ํ•˜๋‚˜์˜ ๋Œ“๊ธ€์€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋Œ“๊ธ€์ข‹์•„์š”๋ฅผ ๊ฐ€์ง€์ง€๋งŒ, ํ•˜๋‚˜์˜ ๋Œ“๊ธ€์ข‹์•„์š”๋Š” ํ•˜๋‚˜์˜ ๋Œ“๊ธ€์—๋งŒ ์†ํ•œ๋‹ค.


๐Ÿค” BaseEntity

๊ณตํ†ต ํ•„๋“œ๋ฅผ ์ •์˜ํ•˜์—ฌ ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ๋“ค์ด ๊ณตํ†ต์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ถ„๋ฆฌํ•œ ์ถ”์ƒ ํด๋ž˜์Šค์ด๋‹ค. ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ๋“ค์€ ํ•ด๋‹น ์ถ”์ƒ ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์•„ ๊ณตํ†ต ํ•„๋“œ๋ฅผ ํŽธ๋ฆฌํ•˜๊ฒŒ ๊ฐ€์ ธ๋‹ค ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(name = "created_at", nullable = false, columnDefinition = "timestamp")
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(name = "updated_at", nullable = false, columnDefinition = "timestamp")
    private LocalDateTime modifiedAt;
}

โœ… @MappedSuperclass

์ด ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›๋Š” ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ๋“ค์ด ๊ณตํ†ต๋œ ํ•„๋“œ๋ฅผ ๊ฐ€์ง€๋„๋ก ํ•จ

โœ… @EntityListeners(AuditingEntityListener.class)

  • JPA Auditing ๊ธฐ๋Šฅ ํ™œ์„ฑํ™”
  • AuditingEntityListener๋Š” ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ๋ฐ ์ˆ˜์ • ์‹œ์ ์„ ์ž๋™์œผ๋กœ ๊ธฐ๋กํ•จ
  • @CreatedDate์™€ @LastModifiedDate๊ฐ€ ์ด ๋ฆฌ์Šค๋„ˆ๋ฅผ ํ†ตํ•ด ์ž๋™์œผ๋กœ ๊ด€๋ฆฌ

โœ… @CreatedDate

  • JPA Auditing ์˜ ์–ด๋…ธํ…Œ์ด์…˜
  • ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์ƒ์„ฑ๋  ๋•Œ ์ž๋™์œผ๋กœ ํ˜„์žฌ ๋‚ ์งœ์™€ ์‹œ๊ฐ„์œผ๋กœ ํ•„๋“œ ์„ค์ •

โœ… @LastModifiedDate

  • JPA Auditing ์˜ ์–ด๋…ธํ…Œ์ด์…˜
  • ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์ˆ˜์ •๋  ๋•Œ ์ž๋™์œผ๋กœ ํ˜„์žฌ ๋‚ ์งœ์™€ ์‹œ๊ฐ„์œผ๋กœ ํ•„๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•จ

โœ… @EnableJpaAuditing

Springboot Application ์ž์ฒด๋„ JpaAuditing ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ๋ณ€๊ฒฝํ•ด์ฃผ์–ด์•ผ ํ•จ

@SpringBootApplication
@EnableJpaAuditing
public class InstagramCloneApplication {

	public static void main(String[] args) {
		SpringApplication.run(InstagramCloneApplication.class, args);
	}

}

๐Ÿค” Builder ํŒจํ„ด

Builder๋ฅผ ์ ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด ํฌ๊ฒŒ 2๊ฐ€์ง€๊ฐ€ ์žˆ์Œ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค.

1. @Builder๋ฅผ ํด๋ž˜์Šค ์ „์ฒด์— ์„ ์–ธํ•˜๋Š” ๋ฐฉ์‹

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Member extends BaseEntity {
  • ํด๋ž˜์Šค์˜ ๋ชจ๋“  ์ƒ์„ฑ์ž์— ๋Œ€ํ•ด ๋นŒ๋” ํŒจํ„ด ์ ์šฉ ๊ฐ€๋Šฅ
  • ํด๋ž˜์Šค ์ „์ฒด์—์„œ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์–ด ์ฝ”๋“œ์˜ ์ผ๊ด€์„ฑ์ด ์œ ์ง€๋จ
  • ์ƒ์„ฑ์ž์— ๋”ฐ๋ผ ๋นŒ๋”๋ฅผ ์ ์šฉํ•˜๋Š” ๋ฐ์— ์žˆ์–ด ํ˜ผ๋™์ด ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ

2. @Builder๋ฅผ ํด๋ž˜์Šค ๋‚ด๋ถ€ ๋ฉ”์†Œ๋“œ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {
    
    (์ƒ๋žต)
   
   @Builder
   public Member(String name, String email, String password) {
      this.name = name;
      this.email = email;
      this.password = password;
   }
}
  • ํด๋ž˜์Šค ๋‚ด์—์„œ ํŠน์ • ์ƒ์„ฑ์ž์— ๋Œ€ํ•ด์„œ๋งŒ ๋นŒ๋” ํŒจํ„ด ์ ์šฉ ๊ฐ€๋Šฅ
  • ๋นŒ๋” ๋ฉ”์„œ๋“œ์—์„œ ํ•„๋“œ๋ฅผ ์„ ํƒ์ ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด ๋” ๋””ํ…Œ์ผํ•œ ์ฝ”๋“œ ์ž‘์„ฑ ๊ฐ€๋Šฅ
  • ์ถ”๊ฐ€์ ์ธ ๋ฉ”์„œ๋“œ ๊ด€๋ฆฌ์™€ ์ฝ”๋“œ ์ค‘๋ณต์„ ๊ฐ์ˆ˜ํ•ด์•ผ ํ•จ

๐Ÿค” @Column ์†์„ฑ

@Column ์–ด๋…ธํ…Œ์ด์…˜์€ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์˜ ํ•„๋“œ์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์˜ ์—ด์„ ๋งคํ•‘ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.

@Column(length = 50, nullable = false)
private String nickname;

@Column(name = "created_at", nullable = false, columnDefinition = "timestamp")
private LocalDateTime createdAt;

name

  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ด ์ด๋ฆ„ ์ง์ ‘ ์ง€์ •
  • ๊ธฐ๋ณธ๊ฐ’์€ ํ•„๋“œ๋ช…

length

  • ์—ด์˜ ์ตœ๋Œ€ ๊ธธ์ด ์„ค์ •
  • length = 50 ์€ varchar(50)์ด๋ผ๋Š” ์˜๋ฏธ

nullable

  • ์—ด์ด NULL ๊ฐ’์„ ํ—ˆ์šฉํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์—ฌ๋ถ€๋ฅผ ์ง€์ •
  • @Column(nullable = false)๋ž€, ํ•ด๋‹น ํ•„๋“œ๋Š” NULL๊ฐ’์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์˜๋ฏธ

columnDefinition

  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ด์˜ SQL ๋ฐ์ดํ„ฐ ์œ ํ˜•๊ณผ ์†์„ฑ์„ ์ง์ ‘ ์ •์˜ํ•  ๋•Œ ์“ฐ์ž„
  • text์™€ timestamp ์œ ํ˜•์˜ ๊ฒฝ์šฐ, ๋”ฐ๋กœ ๋ช…์‹œํ•ด์ฃผ์–ด์•ผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ˜์˜๋จ
  • ์˜ˆ) @Column(columnDefinition = "text")

1-2. Repository ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

@SpringBootTest
@Transactional
class PostRepositoryTest {

โœ… ํด๋ž˜์Šค ์–ด๋…ธํ…Œ์ด์…˜ ์ •๋ฆฌ

  • @SpringBootTest : ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ๋ฅผ ๋กœ๋“œํ•˜์—ฌ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰
  • @Transactional
    • ๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋œ ํ›„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์ƒํƒœ๋ฅผ ๋กค๋ฐฑ
    • ํ…Œ์ŠคํŠธ ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ฐ„์„ญ ๋ฐฉ์ง€ ๊ฐ€๋Šฅ

@BeforeEach
    void ๊ธฐ๋ณธ์„ธํŒ…() {

        // given
        Member member = Member.builder()
                .name("์ดํ•œ์Šฌ")
                .email("[email protected]")
                .password("1234")
                .nickname("sseuldev")
                .build();
        newMember = memberRepository.save(member);

        post1 = Post.builder()
                .content("ํ…Œ์ŠคํŠธ1")
                .member(newMember)
                .build();

                    (์ƒ๋žต)

        postRepository.save(post1);
        postRepository.save(post2);
        postRepository.save(post3);
    }

โœ… @BeforeEach (๊ธฐ๋ณธ์„ธํŒ…)

  • ๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „์— ๋ฐ˜๋“œ์‹œ ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ง€์ •ํ•  ๋•Œ ์‚ฌ์šฉ
  • ๋ชจ๋“  ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๋งˆ๋‹ค ๋ฐ˜๋ณต์ ์ด๊ณ  ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰
  • ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ์ดˆ๊ธฐํ™”ํ•˜๊ฑฐ๋‚˜ ๊ณตํ†ต์œผ๋กœ ํ•„์š”ํ•œ ์„ค์ •์„ ํ•  ๋•Œ ๋งค์šฐ ์œ ์šฉํ•จ
  • ์—ฌ๋Ÿฌ ํ…Œ์ŠคํŠธ์—์„œ ๊ณตํ†ต์ ์œผ๋กœ ํ•„์š”ํ•œ ์„ค์ •์„ ํ•œ ๊ณณ์— ๋ชจ์•„ ์ฝ”๋“œ์˜ ์ค‘๋ณต์„ ์ค„์—ฌ์คŒ
  • @BeforeEach๋กœ ์„ ์–ธ๋œ ๋ฉ”์„œ๋“œ๋Š” ๋ฐ˜๋“œ์‹œ void ํƒ€์ž…์ด์–ด์•ผ ํ•จ!

    @Test
    public void ๊ฒŒ์‹œ๋ฌผ_์กฐํšŒ_ํ…Œ์ŠคํŠธ() throws Exception {

        // given & when
        List<Post> posts = postRepository.findAllByMember(newMember);

        // then
        assertEquals(3, posts.size(), "๊ฒŒ์‹œ๋ฌผ ๊ฐœ์ˆ˜๋Š” ์ด 3๊ฐœ์ž…๋‹ˆ๋‹ค.");

        assertTrue(posts.stream().anyMatch(post -> post.getContent().equals("ํ…Œ์ŠคํŠธ1")));
        assertTrue(posts.stream().anyMatch(post -> post.getContent().equals("ํ…Œ์ŠคํŠธ2")));
        assertTrue(posts.stream().anyMatch(post -> post.getContent().equals("ํ…Œ์ŠคํŠธ3")));

        posts.forEach(post -> assertEquals(newMember.getId(), post.getMember().getId()));
    }

    @Test
    public void ๊ฒŒ์‹œ๋ฌผ_์‚ญ์ œ_ํ…Œ์ŠคํŠธ() throws Exception {

        // given & when
        postRepository.deleteById(post1.getId());

        // then
        List<Post> posts = postRepository.findAllByMember(newMember);

        assertEquals(2, posts.size(), "๊ฒŒ์‹œ๋ฌผ ๊ฐœ์ˆ˜๋Š” ์ด 2๊ฐœ์ž…๋‹ˆ๋‹ค.");

        Optional<Post> deletedPost = postRepository.findById(post1.getId());
        assertFalse(deletedPost.isPresent(), "์‚ญ์ œ๋œ ๊ฒŒ์‹œ๋ฌผ์ด๋ฏ€๋กœ ์กด์žฌํ•˜๋ฉด ์•ˆ๋ฉ๋‹ˆ๋‹ค!");
    }

โœ… Assertions ์ •๋ฆฌ

  • assertEquals (์˜ˆ์ƒ๊ฐ’, ์‹ค์ œ๊ฐ’, ์‹คํŒจ ์‹œ ์ถœ๋ ฅ๋˜๋Š” ๋ฉ”์‹œ์ง€) : ์˜ˆ์ƒ ๊ฐ’๊ณผ ์‹ค์ œ ๊ฐ’์„ ๋น„๊ตํ•˜์—ฌ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ

  • assertTrue (์กฐ๊ฑด, ์‹คํŒจ ์‹œ ์ถœ๋ ฅ๋˜๋Š” ๋ฉ”์‹œ์ง€) : ์ฃผ์–ด์ง„ ์กฐ๊ฑด์ด ์ฐธ์ธ์ง€ ํ™•์ธ

  • assertFalse(์กฐ๊ฑด, ์‹คํŒจ ์‹œ ์ถœ๋ ฅ๋˜๋Š” ๋ฉ”์‹œ์ง€) : ์ฃผ์–ด์ง„ ์กฐ๊ฑด์ด ๊ฑฐ์ง“์ธ์ง€ ํ™•์ธ



2-1. ๐ŸŒฟ์ง€๋‚œ์ฃผ ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง๐ŸŒฟ

โœ๏ธ ํšŒ์› ์‚ญ์ œ Soft Delete ๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ!

image

1. Hard Delete๋ž€?

  • ๋ฌผ๋ฆฌ์  ์‚ญ์ œ
  • ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์ œ๋กœ ์‚ญ์ œํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ๋Š” ์‹œ์Šคํ…œ์—์„œ ์™„์ „ํžˆ ์ œ๊ฑฐ๋˜์–ด ๋ณต๊ตฌ ๋ถˆ๊ฐ€

2. Soft Delete๋ž€?

  • ๋…ผ๋ฆฌ์  ์‚ญ์ œ
  • ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์ œ๋กœ ์‚ญ์ œํ•˜์ง€ ์•Š๊ณ , ์‚ญ์ œ๋œ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ํ•จ
  • ๋ฐ์ดํ„ฐ๋Š” ์‹œ์Šคํ…œ์—์„œ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์ง€๋งŒ, ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋˜๋Œ๋ฆฌ๊ธฐ ๊ฐ€๋Šฅ
  • ๋ฐ์ดํ„ฐ ๋ณด์กด์„ ์œ„ํ•ด ์œ ์šฉํ•˜๋ฉฐ, ์‹ค์ˆ˜๋กœ ์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ ๊ฐ€๋Šฅ
  • ๊ทธ๋Ÿฌ๋‚˜! ์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ ์œ ์ง€ํ•˜๋ ค๋ฉด ์ถ”๊ฐ€์ ์ธ ์ €์žฅ ๊ณต๊ฐ„์ด ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์‹ ์ค‘ํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ๊ฒƒ,,

๐Ÿ‘‰ ํšŒ์›์˜ ๊ฒฝ์šฐ

  • ํƒˆํ‡ด๋ฅผ ํ–ˆ๋‹ค๊ฐ€ ์ผ์ฃผ์ผ ๋‚ด๋กœ ๋‹ค์‹œ ๋Œ์•„์˜ฌ ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ
  • ํšŒ์›์— ๋Œ€ํ•œ ๋ถ„์„, ํ†ต๊ณ„์˜ ํ•„์š”์„ฑ
  • ํƒˆํ‡ด๊ฐ€ ์ด๋ฃจ์–ด์ง„ ๊ฒฝ์šฐ์—๋„, ๋‹ค๋ฅธ ์„œ๋น„์Šค๋‚˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ ์ด ํšŒ์›๊ณผ ๊ด€๋ จ๋œ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ

์ด๋Ÿฌํ•œ ์ด์œ ๋“ค๋กœ ์ธํ•ด Member์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ ์‚ญ์ œ์˜ ๊ฒฝ์šฐ๋Š” Soft Delete ๋ฅผ ์จ์•ผ ํ•œ๋‹ค!!

3. Soft Delete ๊ตฌํ˜„ํ•˜๊ธฐ

  • ์—”ํ‹ฐํ‹ฐ ์‚ญ์ œ๋Š” ์‹ค์ œ DELETE ์ฟผ๋ฆฌ๊ฐ€ ์•„๋‹ˆ๋ผ, UPDATE ๋กœ deleted_at์— ํ˜„์žฌ ์‹œ๊ฐ„(NOW())์„ ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ

    @SqlDelete ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๊ตฌํ˜„

  • ๋ชจ๋“  SQL ์ฟผ๋ฆฌ์— WHERE deleted_at IS NULL์„ ๋ถ™์—ฌ์•ผ๋งŒ ์‚ญ์ œ๋˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์กฐํšŒ, ๋ณ€๊ฒฝ ์ž‘์—… ์ˆ˜ํ–‰ ๊ฐ€๋Šฅ

    @Where ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๊ตฌํ˜„


@SQLDelete(sql = "UPDATE member SET deleted_at = NOW() where id = ?")
@Where(clause = "deleted_at IS NULL")
public class Member extends BaseEntity {

1. @SQLDelete(sql = "UPDATE member SET deleted_at = NOW() where id = ?")

  • ์‚ญ์ œ ์š”์ฒญ์ด ๋“ค์–ด์™”์„ ๋•Œ ์‹คํ–‰๋˜๋Š” SQL ๊ตฌ๋ฌธ ์ •์˜ => UPDATE ์‹คํ–‰
  • ๋ฐ์ดํ„ฐ๋Š” ์‚ญ์ œ๋˜์ง€ ์•Š๊ณ , ์‚ญ์ œ๋œ ์ƒํƒœ ๋กœ ํ‘œ์‹œ ๋จ

2. @Where(clause = "deleted_at IS NULL")

  • deleted_at์ด NULL์ธ, ์ฆ‰ ์‚ญ์ œ๋˜์ง€ ์•Š์€ ๋ ˆ์ฝ”๋“œ๋งŒ ์กฐํšŒ๋˜๊ฒŒ ํ•จ
  • ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ๋Š” ์กฐํšŒ๋˜์ง€ ์•Š์Œ

โœ๏ธ @Builder.Default ์ถ”๊ฐ€ํ•˜๊ธฐ!

* ์›๋ž˜ ํ•˜๋˜ ๋ฐฉ์‹

@Column(name = "comment_count")
private int commentCount = 0;

* @Builder.Default ์ด์šฉํ•œ ๋ฐฉ์‹

@Column(name = "comment_count")
@Builder.Default
private int commentCount = 0;

๋‚ด๊ฐ€ ์›๋ž˜ ํ•˜๋˜ ๋ฐฉ์‹์œผ๋กœ ํ•  ๊ฒฝ์šฐ,

Builder ํŒจํ„ด์—์„œ ๊ธฐ๋ณธ๊ฐ’์„ ์ง์ ‘ ์„ค์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด 0์ด ์•„๋‹Œ ๊ฐ’์œผ๋กœ ์„ค์ •๋  ์—ฌ์ง€๊ฐ€ ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค

ํ”„๋กœ๊ทธ๋žจ ์ „์ฒด์— Builder ํŒจํ„ด์„ ์‚ฌ์šฉํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‚ด๊ฐ€ ์ดˆ๊ธฐํ™”ํ•œ ๊ธฐ๋ณธ๊ฐ’์ด ํ™•์‹คํžˆ ๋ณด์žฅ๋˜๋Š” @Builder.Default ์„ ์ด์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ๋‚˜์€ ๋ฐฉ์‹!!

์ถ”๊ฐ€์ ์œผ๋กœ, ์—”ํ‹ฐํ‹ฐ์˜ new ArrayList<>()์—๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ @Builder.Default๋ฅผ ์ ์šฉํ•ด์ฃผ์—ˆ๋‹ค.

@Builder.Default๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด null ์ƒํƒœ์˜ ๋ฆฌ์ŠคํŠธ์— ์ ‘๊ทผํ•˜๊ฒŒ ๋˜์–ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค!!!

โ— @Builder.Default ์—†์ด Builder๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค๋ฉด,,

ํ•„๋“œ๋Š” ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š๊ณ  null ์ƒํƒœ๋กœ ๋‚จ๊ฒŒ๋œ๋‹ค. ์ด ์ƒํƒœ์—์„œ ์ ‘๊ทผ์„ ์‹œ๋„ํ•œ๋‹ค๋ฉด, NullPointerException ์ด ํ„ฐ์ง€๊ฒŒ ๋œ๋‹ค..

๋”ฐ๋ผ์„œ, @OneToMany ๊ด€๊ณ„์—์„œ List<Post> posts = new ArrayList<>() ์™€ ๊ฐ™์€ ์ปฌ๋ ‰์…˜ ํƒ€์ž… ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํ•ด๋‹น ํ•„๋“œ์— ๋นˆ ์ปฌ๋ ‰์…˜์„ ํ• ๋‹นํ•ด์ค˜์•ผํ•œ๋‹ค.

2-2. Service ์ฝ”๋“œ ๊ตฌํ˜„

๐Ÿค” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ž€?

์ฝ”๋“œ ๋‚ด (์ฃผ๋กœ Service ์ฝ”๋“œ) ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜๋ฅผ ์ฒด๊ณ„์ ์ด๊ณ  ์ผ๊ด€๋˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๊ตฌ์กฐ ๋ฅผ ๋„์ž…ํ•œ๋‹ค๊ณ  ํ•œ๋‹ค.

์˜ˆ์™ธ์ฒ˜๋ฆฌ๊ตฌ์กฐ๋ฅผ ์œ„ํ•ด์„œ๋Š” ์ด 4๊ฐ€์ง€ ํŒŒ์ผ์ด ์š”๊ตฌ๋œ๋‹ค.

1. BadRequestException ํด๋ž˜์Šค

@Getter
public class BadRequestException extends RuntimeException {

    private final int code;
    private final String message;

    public BadRequestException(final ExceptionCode exceptionCode){
        this.code = exceptionCode.getCode();
        this.message = exceptionCode.getMessage();
    }
}
  • ์ž˜๋ชป๋œ ์š”์ฒญ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ์šฉ์ž ์ •์˜ ์˜ˆ์™ธ ํด๋ž˜์Šค
  • ์˜ˆ์™ธ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ํฌํ•จํ•˜๋ฉฐ ExceptionCode์™€ ์—ฐ๋™๋จ

2. ExceptionCode ์—ด๊ฑฐํ˜•

@RequiredArgsConstructor
@Getter
public enum ExceptionCode {

    INVALID_REQUEST(1000, "์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์š”์ฒญ์ž…๋‹ˆ๋‹ค."),

    // ๋ฉค๋ฒ„ ์—๋Ÿฌ
    NOT_FOUND_MEMBER_ID(1001, "์š”์ฒญํ•œ ID์— ํ•ด๋‹นํ•˜๋Š” ๋ฉค๋ฒ„๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
    FAIL_TO_CREATE_NEW_MEMBER(1002, "์ƒˆ๋กœ์šด ๋ฉค๋ฒ„๋ฅผ ์ƒ์„ฑํ•˜๋Š”๋ฐ ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค."),

    // ์ฑ„ํŒ… ์—๋Ÿฌ
    NOT_FOUND_CHATROOM_ID(2001, "์š”์ฒญํ•œ ID์— ํ•ด๋‹นํ•˜๋Š” ์ฑ„ํŒ…๋ฐฉ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
    INVALID_CHATROOM(2002, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฑ„ํŒ…๋ฐฉ์ž…๋‹ˆ๋‹ค."),
    VALID_CHATROOM(2003, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ฑ„ํŒ…๋ฐฉ์ž…๋‹ˆ๋‹ค."),

    // ๊ฒŒ์‹œ๊ธ€ ์—๋Ÿฌ
    NOT_FOUND_POST_ID(3001, "์š”์ฒญํ•œ ID์— ํ•ด๋‹นํ•˜๋Š” ๊ฒŒ์‹œ๊ธ€์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
    NOT_FOUND_POST_LIKE(3002, "์š”์ฒญํ•œ ID์— ํ•ด๋‹นํ•˜๋Š” ๊ฒŒ์‹œ๊ธ€ ์ข‹์•„์š”๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),

    INTERNAL_SERVER_ERROR(9999, "์„œ๋ฒ„ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•ด ์ฃผ์„ธ์š”.");

    private final int code;

    private final String message;
}
  • ๋‹ค์–‘ํ•œ ์˜ˆ์™ธ ์ƒํ™ฉ์— ๋Œ€ํ•œ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ๊ด€๋ฆฌ
  • ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ์ค‘์•™์—์„œ ๊ด€๋ฆฌํ•˜๊ธฐ ๋–„๋ฌธ์— ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ด
  • ์‚ฌ์ „์— ์ •์˜๋œ ์˜ˆ์™ธ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์œ ์ง€ํ•˜๊ฒŒ ํ•จ

3. ExceptionResponse ํด๋ž˜์Šค

@Getter
@RequiredArgsConstructor
public class ExceptionResponse {

    private final int code;
    private final String message;
}
  • ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜ํ•  ์˜ˆ์™ธ ์‘๋‹ต ๊ฐ์ฒด
  • ์˜ˆ์™ธ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌ

4. GlobalExceptionHandler ํด๋ž˜์Šค

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            final MethodArgumentNotValidException e,
            final HttpHeaders headers,
            final HttpStatusCode status,
            final WebRequest request
    ){
        log.warn(e.getMessage(), e);

        final String errorMessage = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage();
        return ResponseEntity.badRequest()
                .body(new ExceptionResponse(INVALID_REQUEST.getCode(), errorMessage));
    }

    @ExceptionHandler(BadRequestException.class)
    public ResponseEntity<ExceptionResponse> handleBadRequestException(final BadRequestException e){
        log.warn(e.getMessage(), e);

        return ResponseEntity.badRequest()
                .body(new ExceptionResponse(e.getCode(), e.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ExceptionResponse> handleException(final Exception e){
        log.error(e.getMessage(), e);

        return ResponseEntity.internalServerError()
                .body(new ExceptionResponse(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMessage()));
    }

}
  • ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํด๋ž˜์Šค
  • ๋‹ค์–‘ํ•œ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ ์ ˆํ•œ ์‘๋‹ต์„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜
  • ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋กœ์ง๊ณผ ์‘๋‹ต ํฌ๋งทํŒ…์„ ์ค‘์•™์—์„œ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ด
  • ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด handleException ๋ฉ”์„œ๋“œ์—์„œ ์ •์˜ํ•œ ์‘๋‹ต ํ˜•์‹๋Œ€๋กœ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜๋จ

โœ… Service ์ฝ”๋“œ์—์„œ๋Š”?

// NOT_FOUND_MEMBER_ID(1001, "์š”์ฒญํ•œ ID์— ํ•ด๋‹นํ•˜๋Š” ๋ฉค๋ฒ„๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
// NOT_FOUND_POST_ID(3001, "์š”์ฒญํ•œ ID์— ํ•ด๋‹นํ•˜๋Š” ๊ฒŒ์‹œ๊ธ€์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),

public Member findMemberById(Long memberId) {
    return memberRepository.findById(memberId).orElseThrow(() -> new BadRequestException(NOT_FOUND_MEMBER_ID));
}

public Post findPostById(Long postId) {
    return postRepository.findById(postId).orElseThrow(() -> new BadRequestException(NOT_FOUND_POST_ID));
}

์ด์™€ ๊ฐ™์ด, ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ๊ตฌ์ฒด์ ์ธ ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€์™€ ์ฝ”๋“œ๊ฐ€ ์ œ๊ณต๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ œ๋ฅผ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค!

[ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฐ›๊ฒŒ ๋˜๋Š” ์˜ค๋ฅ˜์ •๋ณด ์˜ˆ์‹œ ]

{
  "code": 1001,
  "message": "์š”์ฒญํ•œ ID์— ํ•ด๋‹นํ•˜๋Š” ๋ฉค๋ฒ„๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."
}

๐Ÿค” DTO์—์„œ record ์‚ฌ์šฉํ•ด๋ณด๊ธฐ

Service ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉฐ ํ•ญ์ƒ ๋“œ๋Š” ์ƒ๊ฐ์ด..

  • Service ์ฝ”๋“œ์—๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋งŒ ๋‹ด๊ณ  ๊น”๋”ํ•˜๊ฒŒ ํ•˜๊ณ  ์‹ถ๋‹ค!
  • DTO ๋ฅผ ๋” ํšจ์œจ์ ์œผ๋กœ ์ž˜ ์จ๋ณด๊ณ  ์‹ถ๋‹ค!!

์˜€๋‹ค. ๊ธฐ์กด์— ์ž‘์„ฑํ–ˆ๋˜ ๋‚˜์˜ Service ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด..

public MemberEditInfoResponseDto getMemberEditInfo(Long memberId) {

    Member member = findMemberById(memberId);

    return MemberEditInfoResponseDto.builder()
            .imageUrl(member.getImageUrl())
            .name(member.getName())
            .email(member.getEmail())
            .phoneNumber(member.getPhoneNumber())
            .insuranceId(member.getInsurance().getInsuranceId())
            .build();
}

image

์ผ๋‹จ, return ๊ฐ’์— ๋นŒ๋”๊ฐ€ ๋“ค์–ด๊ฐ€์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์ง€๊ณ  ๋ณต์žกํ•ด๋ณด์ธ๋‹ค.

DTO ์˜ ๊ฒฝ์šฐ, ์‚ฌ์šฉํ•˜๋Š” ์ธ์Šคํ„ด์Šค๋“ค์ด ๊ฒน์นจ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ๋ชจ๋“  DTO ํŒŒ์ผ์„ ๊ธฐ๋Šฅ์— ๋”ฐ๋ผ ํ•˜๋‚˜ํ•˜๋‚˜์”ฉ ๋‹ค ๋งŒ๋“ค์–ด๋†“์€ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ’ก Service ์ฝ”๋“œ์˜ builder๋ฅผ ์—†์• ๊ณ  Validation - Business Logic - Response ์— ์ง‘์ค‘ํ•ด์„œ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ๋†’์ด์ž!

๐Ÿ’ก ์“ฐ์ด๋Š” ์ธ์Šคํ„ด์Šค๊ฐ€ ๋น„์Šทํ•œ DTO๋“ค์€ ํ•˜๋‚˜์˜ ํŒŒ์ผ ์•ˆ์— ๋„ฃ๊ณ , DTO ์•ˆ์— builder๋ฅผ ๋„ฃ์–ด๋ณด์ž!

์ด๋Ÿฌํ•œ ์ด์œ ๋“ค๋กœ ๋” ๋‚˜์€ DTO ์ž‘์„ฑ๋ฐฉ์‹์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๋‹ค๊ฐ€ record ๋ฅผ ํ™œ์šฉํ•œ DTO์— ๋Œ€ํ•ด ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค!

DTO์—์„œ record ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์€ ์ด์œ 

  • record๋Š” ๋ณธ๋ž˜ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์„ ์œ„ํ•œ ๋‹จ์ˆœํ•œ ๊ตฌ์กฐ์ฒด ์—ญํ• ์„ ํ•˜๊ธฐ ์œ„ํ•ด ์„ค๊ณ„๋œ ๊ฒƒ
  • ๋ถˆ๋ณ€ ๊ฐ์ฒด๋ฅผ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์–ด์กŒ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋‹จ์ˆœํžˆ ์ „๋‹ฌํ•˜๋Š” DTO์— ์ ํ•ฉ
  • ์ž๋™์œผ๋กœ ์ƒ์„ฑ์ž, getter, equals, hashCode, toString ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณต
  • DTO๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์บก์Šํ™”ํ•˜์—ฌ ์ „์†กํ•˜๋Š” ๊ฐ์ฒด์ด๋ฏ€๋กœ, record์˜ ๊ฐ„๊ฒฐํ•จ๊ณผ ๋ถˆ๋ณ€์„ฑ์ด ํฐ ์žฅ์ 

record์˜ ํŠน์ง•

  • DTO์˜ ํ•„๋“œ๋งŒ ์ •์˜ํ•˜๋ฉด ํ•ด๋‹น ํ•„๋“œ๋ฅผ ํฌํ•จํ•˜๋Š” ์ƒ์„ฑ์ž, getter ๋“ฑ์ด ์ž๋™์œผ๋กœ ์ œ๊ณต
  • ํ•„๋“œ ์ด๋ฆ„ ์ž์ฒด๊ฐ€ getter ์—ญํ• ์„ ํ•˜๋ฏ€๋กœ getName() ๋Œ€์‹  name() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉ
  • record์˜ ํ•„๋“œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ final์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ์ฒด ์ƒ์„ฑ ํ›„ ๊ฐ’ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€
  • record ๋ฅผ ์„ ์–ธํ•  ๋•Œ๋Š” ํ•„์š”ํ•œ ํ•„๋“œ๋ฅผ ์ƒ์„ฑ์ž ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์„ ์–ธ
  • ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ ์ „์†ก์˜ ๊ฒฝ์šฐ record ๊ฐ€ ์ ํ•ฉ!
  • ๋ณต์žกํ•œ ๋กœ์ง์ด๋‚˜ ์ƒ์†์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ class ๊ฐ€ ์ ํ•ฉ!

โœ…record๋ฅผ ํ™œ์šฉํ•œ DTO

public record PostReq(

        @NotNull
        String content,
        int commentCount,
        String location,
        String music,
        @NotNull
        List<String> imageUrls
) {
        public Post toEntity(Member member) {

                List<Image> images = this.imageUrls.stream()
                        .map(imageUrl -> Image.builder()
                                .imageUrl(imageUrl)
                                .post(Post.builder().build())
                                .build())
                        .collect(Collectors.toList());

                return Post.builder()
                        .content(this.content)
                        .commentCount(this.commentCount)
                        .location(this.location)
                        .music(this.music)
                        .member(member)
                        .images(images)
                        .build();
        }
}
@Builder
public record ChatroomRes (
        Long chatroomId,
        Long senderId,
        Long receiverId
) {
    public static ChatroomRes of(Chatroom chatroom) {
        return ChatroomRes.builder()
                .chatroomId(chatroom.getId())
                .senderId(chatroom.getSender().getId())
                .receiverId(chatroom.getReceiver().getId())
                .build();
    }
}
  • of

    • ์ฃผ๋กœ ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋กœ ์‚ฌ์šฉ๋˜์–ด ๊ฐ์ฒด ์ƒ์„ฑ์„ ๋‚˜ํƒ€๋ƒ„
    • ๋‹ค๋ฅธ ๊ฐ์ฒด๋กœ๋ถ€ํ„ฐ ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์‚ฌ์šฉ๋จ
  • toEntity

    • DTO๋ฅผ ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉํ•จ
    • ๋ณ€ํ™˜ ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉ๋จ

์›๋ž˜ record๋ฅผ ํ™œ์šฉํ•œ ๋ฐฉ์‹์—๋Š” ๋ถˆ๋ณ€์„ฑ์„ ์‚ด๋ฆฌ๊ธฐ ์œ„ํ•ด builer ๋ณด๋‹ค๋Š” new ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ํ•œ๋‹ค. ์ถ”ํ›„ ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง์„ ํ•˜๋ฉด์„œ ์ˆ˜์ •ํ•ด๋ด์•ผ๊ฒ ๋‹ค!


[ ํšŒ์› ์ •๋ณด ์ˆ˜์ •์— ๋Œ€ํ•œ ์„œ๋น„์Šค ์ฝ”๋“œ ]

@Transactional
public MemberRes updateMemberInfo(MemberReq request, Long memberId) {

    // Validation
    Member member = findMemberById(memberId);

    // Business Logic
    member.update(request);
    Member saveMember = memberRepository.save(member);

    // Response
    return MemberRes.MemberEditRes(saveMember);
}

๊ทธ ์ด์ „๋ณด๋‹ค ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋” ์ž˜ ๋ณด์ด๊ณ  ๊ฐ€๋…์„ฑ์žˆ๊ฒŒ ์ž‘์„ฑ๋๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!



3-1. ๐ŸŒฟ์ง€๋‚œ์ฃผ ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง๐ŸŒฟ

โœ๏ธ Where ์–ด๋…ธํ…Œ์ด์…˜ ์‚ญ์ œํ•˜๊ธฐ

๊ธฐ์กด์˜ @Where ์–ด๋…ธํ…Œ์ด์…˜์ด deprecated (๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ) ๋˜๊ณ , ๊ทธ ๋Œ€์•ˆ์ฑ…์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์ด @SQLRestriction ์ด๋‹ค!


@SQLRestriction ์ด๋ž€?

  • ํŠน์ • ์—”ํ‹ฐํ‹ฐ ํ•„๋“œ์— ๋Œ€ํ•ด SQL ์กฐ๊ฑด์„ ์„ค์ •ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ
  • ํ•ด๋‹น ํ•„๋“œ์— ๋Œ€ํ•œ ์กฐํšŒ๋‚˜ ์ˆ˜์ •์ด ์ด๋ฃจ์–ด์งˆ ๋•Œ๋งˆ๋‹ค ์ด ์ œ์•ฝ ์กฐ๊ฑด์ด ์ž๋™์œผ๋กœ ๋ฐ˜์˜
  • ํŠนํžˆ ํ•„ํ„ฐ๋ง์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜, ๋…ผ๋ฆฌ์ ์œผ๋กœ ์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ์™ธํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ ๋“ฑ์— ์‚ฌ์šฉ๋จ

โŒ ๊ทธ๋Ÿฌ๋‚˜ @SQLRestriction ์— ๋Œ€ํ•œ ๋ฌธ์ œ์ ์ด ๋งŽ์€ ๊ฒƒ ๊ฐ™์•„, ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ๋กœ ๊ฒฐ์ •ํ•˜์˜€์Œ

  1. ์ œ์•ฝ ์กฐ๊ฑด์ด ๊ณ ์ •์ 
    • @SQLRestriction ์— ์ง€์ •๋œ ์กฐ๊ฑด์€ ๋™์ ์œผ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ, ์กฐ๊ฑด์ด ๊ณ ์ •๋œ ์ƒํ™ฉ์—์„œ๋งŒ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅ
  2. ๋ณต์žกํ•œ ํ•„ํ„ฐ๋ง์—์„œ๋Š” ์–ด๋ ค์›€ ์กด์žฌ
  3. ๋ฐฑ์˜คํ”ผ์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ํ†ต๊ณ„๋ฅผ ๋‚ด๋Š” ๊ฒฝ์šฐ, ์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋ถˆ๊ฐ€
    • Soft Delete ๋ฐฉ์‹์„ ์„ ํƒํ•œ ์˜๋ฏธ๊ฐ€ ์‚ฌ๋ผ์ง

๐Ÿค” ์ด๊ฑธ ์•ˆ์“ฐ๋ฉด ์–ด๋–ป๊ฒŒ ํ• ๊ฑฐ์•ผ?

Optional<Post> findByIdAndDeletedAtIsNull(Long postId);

์กฐ๊ฑด๋ถ€ ์ฟผ๋ฆฌ ๋ฉ”์†Œ๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ์‹์„ ์„ ํƒํ•˜์˜€๋‹ค.

deletedAt์ด NULL์ธ ๊ฒฝ์šฐ๋งŒ ์กฐํšŒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ช…์‹œ์ ์ด๊ณ  ์œ ์—ฐํ•œ ์กฐ๊ฑด ์„ค์ •์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค!


โœ๏ธ List ํƒ€์ž… ์ด ์ •๋ฆฌ

1. Stream ์ด๋ž€?

์ปฌ๋ ‰์…˜(๋ฐฐ์—ด ํฌํ•จ)์˜ ์ €์žฅ ์š”์†Œ๋ฅผ ํ•˜๋‚˜์”ฉ ์ฐธ์กฐํ•ด์„œ ๋žŒ๋‹ค์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” ๋ฐ˜๋ณต์ž

  • ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ณ€ํ™˜ (stream() ๋ฉ”์„œ๋“œ)
  • ์ค‘๊ฐ„ ์—ฐ์‚ฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ ๋˜๋Š” ํ•„ํ„ฐ๋ง
  • ์ข…๋ฃŒ ์—ฐ์‚ฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๋ชจ์œผ๊ฑฐ๋‚˜ ์ฒ˜๋ฆฌ

โœ”๏ธ ์ค‘๊ฐ„ ์—ฐ์‚ฐ

  • map(Function) โ†’ ๊ฐ ์š”์†Œ๋ฅผ ๋‹ค๋ฅธ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜
  • filter(Predicate) โ†’ ์กฐ๊ฑด์— ๋งž๋Š” ์š”์†Œ๋งŒ ์„ ํƒ
  • sorted() โ†’ ์š”์†Œ๋“ค์„ ์ •๋ ฌ

โœ”๏ธ ์ข…๋ฃŒ ์—ฐ์‚ฐ

  • collect(Collector) โ†’ ์ŠคํŠธ๋ฆผ์˜ ์š”์†Œ๋ฅผ ์ˆ˜์ง‘ํ•˜์—ฌ ๋ฆฌ์ŠคํŠธ๋‚˜ ์„ธํŠธ๋กœ ๋ฐ˜ํ™˜
  • forEach(Consumer) โ†’ ๊ฐ ์š”์†Œ์— ๋Œ€ํ•ด ํŠน์ • ์ž‘์—…์„ ์ˆ˜ํ–‰
  • reduce(BinaryOperator) โ†’ ์š”์†Œ๋ฅผ ๋ฐ˜๋ณต์ ์œผ๋กœ ๊ฒฐํ•ฉํ•˜์—ฌ ๋‹จ์ผ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑ
List<Integer> studentIds = students.stream()
        .filter(student -> student.getGrade() >= 90) // ์ ์ˆ˜๊ฐ€ 90 ์ด์ƒ์ธ ํ•™์ƒ๋งŒ ํ•„ํ„ฐ๋ง
        .sorted(Comparator.comparing(Student::getAge)) // ๋‚˜์ด ์ˆœ์„œ๋กœ ์ •๋ ฌ
        .map(Student::getId) // ํ•™์ƒ์˜ ID๋งŒ ์ถ”์ถœ
        .collect(Collectors.toList()); // ID๋ฅผ List๋กœ ์ˆ˜์ง‘

2. ๋ฉ”์„œ๋“œ ๋ ˆํผ๋Ÿฐ์Šค (map ํ•จ์ˆ˜)

// ๋ฉ”์„œ๋“œ ๋ ˆํผ๋Ÿฐ์Šค
.map(this::findOrCreateHashtag)
.map(HashtagResponseDto::from)

// ๊ธฐ์กด
.map(hashtag -> this.findOrCreateHashtag(hashtag))
.map(hashtag -> HashtagResponseDto.from(hashtag))

3. Collectors.toList() vs Stream.toList()

โ˜‘๏ธ Collectors.toList()

  • Java 8์—์„œ ๋„์ž…๋œ ๋ฉ”์„œ๋“œ๋กœ, ์ŠคํŠธ๋ฆผ์˜ ์š”์†Œ๋“ค์„ ๋ฆฌ์ŠคํŠธ๋กœ ์ˆ˜์ง‘ํ•˜๋Š” ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ๋ฐฉ๋ฒ•
  • ์ŠคํŠธ๋ฆผ์—์„œ ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ๋ฅผ List๋กœ ๋ณ€ํ™˜ํ•˜๋Š” Collector
stream.collect(Collectors.toList());

โ˜‘๏ธ Stream.toList()

  • Java 16์—์„œ ์ƒˆ๋กญ๊ฒŒ ๋„์ž…๋œ ๋ฉ”์„œ๋“œ๋กœ, ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉ

  • Collectors.toList()์™€ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ํ•˜๋ฉฐ ๋” ๊ฐ„๊ฒฐํ•œ ๋ฌธ๋ฒ•์„ ์ œ๊ณต

  • Stream.toList()๋Š” ๋ถˆ๋ณ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ˜๋ฉด, Collectors.toList()๋Š” ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜

    โ‡’ Stream.toList()๋กœ ๋ฐ˜ํ™˜๋œ ๋ฆฌ์ŠคํŠธ๋Š” ์ˆ˜์ •ํ•  ์ˆ˜ ์—†์Œ


โ˜‘๏ธ ๋น„๊ต

  • Collectors.toList()๋Š” ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜

    โœ”๏ธ ๋ฐ˜ํ™˜๋œ ๋ฆฌ์ŠคํŠธ์—์„œ ์š”์†Œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Œ

  • Stream.toList()๋Š” ๋ถˆ๋ณ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Œ

๋ถˆ๋ณ€ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜์ด ๊ฐ€๋Šฅํ•˜๊ณ  ํ˜•ํƒœ๊ฐ€ ๋” ๊ฐ„๊ฒฐํ•œ Stream.toList() ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •!

.imageUrls(post.getImages().stream()
.map(Image::getImageUrl)  // ๊ฐ Image ๊ฐ์ฒด์—์„œ imageUrl ๊ฐ’์„ ์ถ”์ถœํ•˜์—ฌ ์ƒˆ๋กœ์šด ์ŠคํŠธ๋ฆผ ์ƒ์„ฑ
.toList())  // ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜

โœ๏ธ dto๋กœ ์•Œ์•„๋ณด๋Š” public๊ณผ public static์˜ ์ฐจ์ด

1. public (์ธ์Šคํ„ด์Šค ๋ฉ”์„œ๋“œ)

public Chatroom toEntity(Member sender, Member receiver) {
    return Chatroom.builder()
                .sender(sender)
                .receiver(receiver)
                .build();
}
  • ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ๋จผ์ € ์ƒ์„ฑ ํ›„ ํ˜ธ์ถœ ๊ฐ€๋Šฅ
  • ํŠน์ • ๊ฐ์ฒด์˜ ์ƒํƒœ๋ฅผ ๋ณ€ํ™˜ํ•˜๊ฑฐ๋‚˜ ๊ทธ ๊ฐ์ฒด์™€ ๋ฐ€์ ‘ํ•œ ๊ด€๋ จ์ด ์žˆ์„ ๋•Œ ์ฃผ๋กœ ์‚ฌ์šฉ
  • ๊ฐ์ฒด์˜ ์ƒํƒœ์— ๋”ฐ๋ผ ๋™์ž‘ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉ
  • ์˜ˆ) toEntity๋Š” ChatroomRequestDto ๊ฐ์ฒด๊ฐ€ ๋งŒ๋“ค์–ด์ง„ ํ›„์— ๊ทธ ๊ฐ์ฒด์— ์ ‘๊ทผํ•ด ํ˜ธ์ถœ๋จ
MyClass obj = new MyClass();    // ๊ฐ์ฒด๊ฐ€ ๋งŒ๋“ค์–ด์ ธ์•ผ
obj.instanceMethod();           // ํ˜ธ์ถœ ๊ฐ€๋Šฅ

2. public static (์ •์  ๋ฉ”์„œ๋“œ)

public static ChatroomResponseDto from(Chatroom chatroom) {
        return ChatroomResponseDto.builder()
                .chatroomId(chatroom.getId())
                .senderId(chatroom.getSender().getId())
                .receiverId(chatroom.getReceiver().getId())
                .createdAt(chatroom.getCreatedAt())
                .build();
    }
  • ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ ๋„, ํด๋ž˜์Šค ์ž์ฒด์—์„œ ํ˜ธ์ถœ ๊ฐ€๋Šฅ

  • ํŠน์ • ์ธ์Šคํ„ด์Šค์— ์˜์กดํ•˜์ง€ ์•Š์Œ

  • ๊ฐ์ฒด์™€ ๊ด€๊ณ„์—†์ด ํด๋ž˜์Šค ๋ ˆ๋ฒจ์—์„œ ๊ณตํ†ต์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉ

  • ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ํŒจํ„ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ฐฉ์‹

    ๐Ÿ‘‰ ์™œ? ์ธ์Šคํ„ด์Šค๊ฐ€ ์—†๋”๋ผ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ธ์Šคํ„ด์Šค ์ž์ฒด์˜ ์ƒํƒœ์™€ ๊ด€๋ จ์ด ์—†๊ธฐ ๋•Œ๋ฌธ

MyClass.printMessage();

โœ๏ธ cascade ์†์„ฑ๊ณผ orphan ์†์„ฑ ์ดํ•ดํ•˜๊ธฐ

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Image> images = new ArrayList<>();

Post ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ €์žฅํ•  ๋•Œ ์—ฐ๊ด€๋œ Image ์—”ํ‹ฐํ‹ฐ๋“ค๋„ ์ž๋™์œผ๋กœ ์ €์žฅ๋จ.

โœ… ๋”ฐ๋ผ์„œ imageRepository.saveAll(images)๋ฅผ ๋”ฐ๋กœ ํ˜ธ์ถœํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค!!

  • cascade = CascadeType.ALL

    • Post ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์ €์žฅ๋˜๊ฑฐ๋‚˜ ์ˆ˜์ •, ์‚ญ์ œ๋  ๋•Œ ์—ฐ๊ด€๋œ Image ์—”ํ‹ฐํ‹ฐ๋“ค๋„ ํ•จ๊ป˜ ์ฒ˜๋ฆฌ๋œ๋‹ค๋Š” ์˜๋ฏธ
    • ์ฆ‰, Post๋ฅผ ์ €์žฅํ•˜๋ฉด ๊ทธ์— ์†ํ•œ Image ๋ฆฌ์ŠคํŠธ๋„ ํ•จ๊ป˜ ์ €์žฅ
  • orphanRemoval = true

    • Post์— ์†ํ•œ Image๊ฐ€ Post ์—”ํ‹ฐํ‹ฐ์—์„œ ์ œ๊ฑฐ๋˜๋ฉด ์ž๋™์œผ๋กœ ํ•ด๋‹น Image๋„ ์‚ญ์ œ๋œ๋‹ค๋Š” ์˜๋ฏธ
    • ์ฆ‰, ๊ณ ์•„ ์ƒํƒœ๊ฐ€ ๋œ Image ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ž๋™์œผ๋กœ ์ œ๊ฑฐํ•ด์คŒ

๐Ÿ’ก Post์™€ ์—ฐ๊ด€๋œ Image ์—”ํ‹ฐํ‹ฐ๋“ค์€ ์ž๋™์œผ๋กœ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ์ €์žฅ๋˜๊ฑฐ๋‚˜ ์‚ญ์ œ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ์‹ ๊ฒฝ์จ์ฃผ์ง€ ์•Š์•„๋„ ๋จ!


3-2. Controller ์ฝ”๋“œ ๊ตฌํ˜„

๐Ÿค” ์„ฑ๊ณต ์‘๋‹ต ์ฝ”๋“œ ํ†ต์ผํ•˜๊ธฐ

@Data
@AllArgsConstructor
public class CommonResponse<T> {

    private int code;
    private boolean inSuccess;
    private String message;
    private T result;

    public CommonResponse(ResponseCode status, T result) {
        this.code = status.getCode();
        this.inSuccess = status.isInSuccess();
        this.message = status.getMessage();
        this.result = result;
    }

    public CommonResponse(ResponseCode status) {
        this.code = status.getCode();
        this.inSuccess = status.isInSuccess();
        this.message = status.getMessage();
    }
}
@Getter
public enum ResponseCode {

    SUCCESS(2000, true, "์š”์ฒญ์— ์„ฑ๊ณตํ•˜์˜€์Šต๋‹ˆ๋‹ค.");

    private int code;
    private boolean inSuccess;
    private String message;

    ResponseCode(int code, boolean inSuccess, String message) {
        this.inSuccess = inSuccess;
        this.code = code;
        this.message = message;
    }
}

Controller ์ฝ”๋“œ

@Operation(summary = "๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ", description = "ํ•˜๋‚˜์˜ ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ•˜๋Š” API")
@GetMapping("/{postId}")
public CommonResponse<PostResponseDto> getPost(@PathVariable Long postId) {

    return new CommonResponse<>(ResponseCode.SUCCESS, postService.getPost(postId));
}

ํ”„๋ก ํŠธ์—”๋“œ์—๊ฒŒ ์ „ํ•ด์ง€๋Š” ์‹ค์ œ ์‘๋‹ต

์Šคํฌ๋ฆฐ์ƒท 2024-10-28 025755


๐Ÿค” ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… < ์ผ๋Œ€์ผ ์ฑ„ํŒ… ์กฐํšŒ >

๋ฌธ์ œ์ 

์‚ฌ์šฉ์ž๊ฐ€ ์ฐธ์—ฌํ•œ ์ผ๋Œ€์ผ ์ฑ„ํŒ…๋ฐฉ์„ ์กฐํšŒํ•˜๋Š” API๋ฅผ ์ž‘์„ฑํ•˜๋˜ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•œ ๋ฌธ์ œ์ด๋‹ค.

@Operation(summary = "1:1 ์ฑ„ํŒ…๋ฐฉ ์กฐํšŒ", description = "1:1 ์ฑ„ํŒ…๋ฐฉ์„ ์กฐํšŒํ•˜๋Š” API")
@GetMapping("/{senderId}/{receiverId}")
public CommonResponse<ChatroomResponseDto> getChatroom(@PathVariable Long senderId, @PathVariable Long receiverId) {

    return new CommonResponse<>(ResponseCode.SUCCESS, chatService.getChatroom(senderId, receiverId));
}

sender ์™€ receiver ๋ฅผ ๊ตฌ๋ถ„ํ•ด์„œ ์ธ์ž๋ฅผ ๋ฐ›๊ณ  ์„œ๋น„์Šค ์ฝ”๋“œ์—์„œ ์กฐํšŒํ•˜๋Š” ๋กœ์ง์œผ๋กœ ๊ตฌ์„ฑํ•˜์˜€๋‹ค. ์ด๋•Œ, ์‹ค์ œ๋กœ๋Š” sender, receiver ๊ตฌ๋ถ„ ์—†์ด ๋‚ด๊ฐ€ ๋Œ€ํ™”๋ฅผ ์ฐธ์—ฌํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ์ฑ„ํŒ…๋ฐฉ ์กฐํšŒ๊ฐ€ ๋˜์–ด์•ผํ•œ๋‹ค!

๋”ฐ๋ผ์„œ ์„œ๋น„์Šค ๋กœ์ง์—์„œ๋Š”, findBySenderAndReceiverOrReceiverAndSender(sender, receiver, receiver, sender) ๋ฅผ ์ด์šฉํ•˜์—ฌ sender์™€ receiver์˜ ์œ„์น˜์— ๊ด€๊ณ„์—†์ด ์ฑ„ํŒ…๋ฐฉ์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ•˜์˜€๋‹ค.

ํ•˜์ง€๋งŒ swagger ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณด๋‹ˆ ๋‚ด๊ฐ€ sender ์ธ ๊ฒฝ์šฐ๋งŒ ์ฑ„ํŒ…๋ฐฉ ์กฐํšŒ๊ฐ€ ๋˜๊ณ , receiver ์˜ ๊ฒฝ์šฐ์—๋Š” ์ฑ„ํŒ…๋ฐฉ ์กฐํšŒ๊ฐ€ ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€๋‹ค.


๋ฌธ์ œ์˜ ์›์ธ

๋ฌธ์ œ๋Š” ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ JPA ๋ฉ”์„œ๋“œ ์ฟผ๋ฆฌ์— ์žˆ์—ˆ๋‹ค.

๋ณด๋‹ค์‹œํ”ผ ํ•ด๋‹น ๋ฉ”์„œ๋“œ ์ฟผ๋ฆฌ๋Š” AND ๊ณผ OR ์ด ๋ณต์žกํ•˜๊ฒŒ ํ˜ผํ•ฉ๋˜์–ด์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. JPA์—์„œ๋Š” OR ์กฐ๊ฑด์„ ํฌํ•จํ•œ ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ, AND ์™€ OR ์˜ ์กฐํ•ฉ์„ ์ •ํ™•ํžˆ ํ•ด์„ํ•˜์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค. ์ด๋กœ ์ธํ•ด ๋‚ด ์˜๋„์™€๋Š” ๋‹ค๋ฅด๊ฒŒ ์ฝ”๋“œ๊ฐ€ ๋™์ž‘ํ•œ ๊ฒƒ์ด๋‹ค.


ํ•ด๊ฒฐ์ฑ…

JPQL๋„ ์งœ๋ฒ„๋ฆ‡ ํ•˜์ž

๐Ÿ’ก@Query ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์„œ JPQL๋กœ ์ž‘์„ฑํ•˜์ž

@Query("SELECT c FROM Chatroom c WHERE " +
       "((c.sender = :sender AND c.receiver = :receiver) OR " +
       "(c.sender = :receiver AND c.receiver = :sender)) AND c.deletedAt IS NULL")
Optional<Chatroom> findChatroomByMembers(@Param("sender") Member sender, @Param("receiver") Member receiver)
  • @Query
    • JPA Repository ๋ฉ”์„œ๋“œ์—์„œ ์ง์ ‘ ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ์–ด๋…ธํ…Œ์ด์…˜
  • sender ์™€ receiver ์˜ ์œ„์น˜๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ๋™์ผํ•œ ์ฑ„ํŒ…๋ฐฉ์ด ์กฐํšŒ๋˜๋„๋ก ํ•ด๋ผ.
  • @Param("sender")์™€ @Param("receiver")
    • @Param ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด sender์™€ receiver ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฟผ๋ฆฌ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•จ

3-3. Controller ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„

1. ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ์„ค์ •

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class PostControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;
  • @SpringBootTest ์™€ @AutoConfigureMockMvc

    • SpringBoot ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์„ฑ
    • MockMvc ๋ฅผ ์ž๋™ ์„ค์ •ํ•˜์—ฌ ํ…Œ์ŠคํŠธ ์ค‘ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ๋ฅผ ๋กœ๋“œํ•˜๊ณ , ์ปจํŠธ๋กค๋Ÿฌ์˜ ์‹ค์ œ ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ
  • @Transactional

    • ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๋ฉด DB ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ž๋™์œผ๋กœ ๋กค๋ฐฑํ•˜์—ฌ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๊นจ๋—ํ•˜๊ฒŒ ์œ ์ง€ํ•˜๋Š” ์—ญํ• 
  • ObjectMapper

    • Java ๊ฐ์ฒด์™€ JSON ๊ฐ„์˜ ๋ณ€ํ™˜์„ ๋‹ด๋‹น
    • ์š”์ฒญ ๋ณธ๋ฌธ์œผ๋กœ ๊ฐ์ฒด๋ฅผ JSON์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜, JSON ์‘๋‹ต์„ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉ

2. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

@Test
public void ๊ฒŒ์‹œ๋ฌผ_์ƒ์„ฑ_์„ฑ๊ณต() throws Exception {

    // given
    List<String> images = List.of("aaa", "bbb");
    PostRequestDto request = new PostRequestDto("์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๊ธ€์ž…๋‹ˆ๋‹ค",0, "์„œ์šธ์‹œ", "music", images);

    // when & then
    mockMvc.perform(post("/api/post/{memberId}", memberId)
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(request)))

            .andDo(print()) // ์š”์ฒญ๊ณผ ์‘๋‹ต ์ •๋ณด๋ฅผ ์ฝ˜์†”์— ์ถœ๋ ฅ
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.result.content").value("์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๊ธ€์ž…๋‹ˆ๋‹ค"))
            .andExpect(jsonPath("$.result.location").value("์„œ์šธ์‹œ"))
            .andExpect(jsonPath("$.result.music").value("music"))
            .andExpect(jsonPath("$.result.imageUrls[0]").value("aaa"))
            .andExpect(jsonPath("$.result.imageUrls[1]").value("bbb"));
}
  • mockMvc.perform

    • MockMvc ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด HTTP ์š”์ฒญ์„ ์‹คํ–‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ
  • contentType(MediaType.APPLICATION_JSON)

    • ์š”์ฒญ์˜ Content-Type์ด JSON ํ˜•์‹์ž„์„ ๋ช…์‹œ
  • content(objectMapper.writeValueAsString(request))

    • PostRequestDto ๊ฐ์ฒด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์š”์ฒญ ๋ณธ๋ฌธ์— ํฌํ•จ์‹œํ‚ด
  • andDo(print())

    • ์š”์ฒญ๊ณผ ์‘๋‹ต ์ •๋ณด๋ฅผ ์ฝ˜์†”์— ์ถœ๋ ฅ
  • andExpect ๋ฉ”์„œ๋“œ

    • ์„œ๋ฒ„์˜ ์‘๋‹ต์ด ์˜ˆ์ƒํ•œ ๊ฒฐ๊ณผ์™€ ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฒ€์ฆ
    • status().isOk() ๋Š” ์‘๋‹ต HTTP ์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200 OK ์ธ์ง€ ํ™•์ธ
    • jsonPath("$.result.content").value("์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๊ธ€์ž…๋‹ˆ๋‹ค")
      • ์‘๋‹ต JSON์˜ result.content ํ•„๋“œ ๊ฐ’์ด ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ


4. Spring Security

4-1. JWT ์ธ์ฆ(Authentication)

jwt

JWT๋ฅผ ์ด์šฉํ•œ ์ธ์ฆ ๋ฐฉ์‹์—์„œ๋Š” accessToken ๊ณผ refreshToken ์„ ํ™œ์šฉํ•œ๋‹ค.

โญ JWT ์ธ์ฆ ํ๋ฆ„ ์š”์•ฝ

  1. ๋กœ๊ทธ์ธ ์‹œ, accessToken๊ณผ refreshToken ๋ฐœ๊ธ‰
    • ์ด๋•Œ, refreshToken์€ accessToken๋ณด๋‹ค ํ† ํฐ ๋งŒ๋ฃŒ ๊ธฐ๊ฐ„์ด ๋” ๊ธธ๋‹ค!
  2. accessToken์œผ๋กœ ์ธ์ฆ
    • ํด๋ผ์ด์–ธํŠธ๋Š” API ์š”์ฒญ ์‹œ accessToken์„ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„์— ์‚ฌ์šฉ์ž๋ฅผ ์ธ์ฆ
  3. accessToken ๋งŒ๋ฃŒ ์‹œ refreshToken ์‚ฌ์šฉ
    • accessToken์ด ๋งŒ๋ฃŒ๋˜๋ฉด, ํด๋ผ์ด์–ธํŠธ๋Š” refreshToken์„ ์‚ฌ์šฉํ•ด ์ƒˆ๋กœ์šด accessToken์„ ์š”์ฒญ

    • ์„œ๋ฒ„๋Š” DB์— ์ €์žฅ๋œ refreshToken๊ณผ ๋น„๊ตํ•˜์—ฌ ์œ ํšจํ•œ ๊ฒฝ์šฐ, ์ƒˆ๋กœ์šด accessToken ๋ฐœ๊ธ‰

      โœ… ๊ฒ€์ฆ์„ ์œ„ํ•ด์„œ ์„œ๋ฒ„์— refreshToken์„ ๋ณ„๋„๋กœ ์ €์žฅ์‹œํ‚ค๋Š” ๋กœ์ง ํ•„์š”!

  4. refreshToken ๋งŒ๋ฃŒ ์‹œ ์žฌ๋กœ๊ทธ์ธ ํ•„์š”
    • refreshToken ๋˜ํ•œ ๋งŒ๋ฃŒ๋˜๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด, ์‚ฌ์šฉ์ž๋Š” ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์•ผ ํ•จ

โญ JWT ์ƒ์„ฑ ์›๋ฆฌ ๋ฐ ์•”ํ˜ธํ™” ๋ฐฉ์‹

Header + Payload + Signature ๊ตฌ์กฐ

๋‚ด๋ถ€ ์ •๋ณด๋ฅผ ๋‹จ์ˆœ BASE64 ๋ฐฉ์‹์œผ๋กœ ์ธ์ฝ”๋”ฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์™ธ๋ถ€์—์„œ ์‰ฝ๊ฒŒ ๋””์ฝ”๋”ฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

๋”ฐ๋ผ์„œ, ์™ธ๋ถ€์—์„œ ์—ด๋žŒํ•ด๋„ ๋˜๋Š” ์ •๋ณด๋งŒ์„ ๋‹ด์•„์•ผ ํ•œ๋‹ค!

โŒ ํ† ํฐ ๋‚ด๋ถ€์— ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๊ฐ™์€ ๊ฐ’ ์ž…๋ ฅ ๊ธˆ์ง€

  • ํ† ํฐ ์ž์ฒด์˜ ๋ฐœ๊ธ‰์ฒ˜๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉ

  • (๋‚ด๊ฐ€ ์„ ํƒํ•œ) ์•”ํ˜ธํ™” ๋ฐฉ์‹ : ์–‘๋ฐฉํ–ฅ ๋Œ€์นญํ‚ค ๋ฐฉ์‹์ธ HS256 ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •

    โœ… ์•”ํ˜ธํ™” ํ‚ค๋ฅผ ๋”ฐ๋กœ application.yml ํŒŒ์ผ์— ์ €์žฅํ•ด๋‘์–ด์•ผ ํ•จ (์œ ์ถœ ๋ฐฉ์ง€ ์œ„ํ•จ)


4-2. ์„ธ์…˜, ํ† ํฐ, ์ฟ ํ‚ค, OAuth ๋ฐฉ์‹ ๋น„๊ต

'๋ˆ„๊ฐ€' ๋กœ๊ทธ์ธ ์ค‘์ธ์ง€์— ๋Œ€ํ•œ ์ƒํƒœ๋ฅผ ๊ธฐ์–ตํ•˜๊ธฐ ์œ„ํ•ด ์„ธ์…˜, ํ† ํฐ, ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

1. ์„ธ์…˜

  • ์„œ๋ฒ„ ์ค‘์‹ฌ์˜ ์ธ์ฆ ๋ฐฉ์‹ โ‡’ ์„œ๋ฒ„์— ์‚ฌ์šฉ์ž ์ƒํƒœ๋ฅผ ์ €์žฅ

  • ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๊ฐ™์€ ์ธ์ฆ ์ •๋ณด๋ฅผ ์ฟ ํ‚ค์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ , ๋Œ€์‹ ์— ์‚ฌ์šฉ์ž์˜ ์‹๋ณ„์ž์ธ session Id ๋ฅผ ์ €์žฅ

    โ‡’ session id๋ฅผ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ์™€ ์†Œํ†ตํ•˜๋Š” ํ˜•์‹

  • ๋ณด์•ˆ์„ฑ์ด ๋†’์€ ๋ฐ˜๋ฉด, ์ €์žฅ์†Œ๊ฐ€ ๊ณผ๋ถ€ํ•˜๋  ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ

2. ํ† ํฐ

  • ํด๋ผ์ด์–ธํŠธ ์ธก์— ํ† ํฐ ์ •๋ณด ์ €์žฅ

  • ์š”์ฒญ ํ—ค๋”์— ์ง์ ‘ ํฌํ•จํ•˜์—ฌ ์ „์†ก

  • ์„œ๋ฒ„๋Š” ๋ฌด์ƒํƒœ (Stateless) ๋กœ ๋™์ž‘

    โ“ Stateless : ์„œ๋ฒ„๊ฐ€ ๊ฐ ์š”์ฒญ์— ๋Œ€ํ•œ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๋Š” ๋ฐฉ์‹

  • ํ™•์žฅ์„ฑ๊ณผ ์„ฑ๋Šฅ์ด ๋›ฐ์–ด๋‚˜์ง€๋งŒ, ๋ณด์•ˆ ์ธก๋ฉด์—์„œ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”

  • ์ผ์ •ํ•œ ํ† ํฐ ์œ ํšจ๊ธฐ๊ฐ„ ๋™์•ˆ์˜ ํ† ํฐ ๋ณด์•ˆ ๊ด€๋ฆฌ ํ•„์š”

3. ์ฟ ํ‚ค

  • ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„์˜ ์ƒํƒœ ์ •๋ณด๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
  • ์ฟ ํ‚ค๋Š” ๊ณต๊ฐœ ๊ฐ€๋Šฅํ•œ ์ •๋ณด๋ฅผ ์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅ์‹œํ‚ด
  • ์‚ฌ์šฉ์ž๋ฅผ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ๋Š” ํ† ํฐ (refreshToken) ์ด๋‚˜ session ID ๊ฐ™์€ ์‹๋ณ„ ์ •๋ณด๋ฅผ ์ €์žฅ
  • ์„œ๋ฒ„์— ๋ถ€๋‹ด์ด ์—†๊ณ  ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๋ณด์•ˆ์— ์ทจ์•ฝํ•˜๊ณ  ํด๋ผ์ด์–ธํŠธ์ธก์œผ๋กœ๋ถ€ํ„ฐ ์กฐ์ž‘๋  ๊ฐ€๋Šฅ์„ฑ์ด ์กด์žฌํ•œ๋‹ค๋Š” ๋‹จ์  ์กด์žฌ

โœ”๏ธ ๊ณผ์ •

  1. ์„œ๋ฒ„๋Š” ํด๋ผ์ด์–ธํŠธ์˜ ๋กœ๊ทธ์ธ ์š”์ฒญ์— ๋Œ€ํ•œ ์‘๋‹ต์„ ์ž‘์„ฑํ•  ๋•Œ, ํด๋ผ์ด์–ธํŠธ ์ธก์— ์ €์žฅํ•˜๊ณ  ์‹ถ์€ ์ •๋ณด๋ฅผ ์‘๋‹ต ํ—ค๋”์˜ย set-cookieย ์— ๋‹ด์•„ ์‘๋‹ต
  2. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฟ ํ‚ค๋ฅผ ์ €์žฅํ•˜๊ณ  ์ดํ›„ ๋ชจ๋“  ์š”์ฒญ๋งˆ๋‹ค ์ฟ ํ‚ค๋ฅผ ์„œ๋ฒ„๋กœ ๋‹ค์‹œ ์ „์†กํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘
  3. ์„œ๋ฒ„๋Š” ์ฟ ํ‚ค์— ๋‹ด๊ธด ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ•ด๋‹น ์š”์ฒญ์˜ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ˆ„๊ตฐ์ง€ ์‹๋ณ„

4. OAuth

  • ์ธ์ฆ์˜ ๊ณผ์ •์„ 'ํƒ€ ์„œ๋น„์Šค์—๊ฒŒ ์œ„์ž„' ํ•˜๋Š” ์ธ์ฆ ๋ฐฉ์‹ (์˜ˆ: ์†Œ์…œ๋กœ๊ทธ์ธ)

โœ”๏ธ ๊ณผ์ •

  1. ์‚ฌ์šฉ์ž ์š”์ฒญ

    • ํด๋ผ์ด์–ธํŠธ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„ (์˜ˆ: Google, Facebook) ์— ์ ‘๊ทผํ•˜๊ณ ์ž ํ•˜๋Š” ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž๋ฅผ OAuth ์ธ์ฆ ์„œ๋ฒ„๋กœ ๋ฆฌ๋””๋ ‰์…˜ํ•˜์—ฌ ์ ‘๊ทผ ๊ถŒํ•œ์„ ์š”์ฒญ
  2. ์‚ฌ์šฉ์ž ์Šน์ธ

    • ์‚ฌ์šฉ์ž๋Š” OAuth ์„œ๋ฒ„์—์„œ ๋กœ๊ทธ์ธํ•˜๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ž์‹ ์˜ ์ •๋ณด์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์„ ์Šน์ธ
  3. Authorization Code ๋ฐœ๊ธ‰

    • ์ธ์ฆ ์„œ๋ฒ„๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์Šน์ธํ•œ ํ›„, ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ Authorization Code ๋ฅผ ๋ฐœ๊ธ‰ํ•˜์—ฌ ์ „๋‹ฌ
    • ์ด ์ฝ”๋“œ๋Š” ์ผํšŒ์šฉ์ด๋ฉฐ ์งง์€ ์‹œ๊ฐ„ ๋™์•ˆ๋งŒ ์œ ํšจ
  4. accessToken ์š”์ฒญ

    • ํด๋ผ์ด์–ธํŠธ๋Š” Authorization Code ์™€ ํ•จ๊ป˜ ์ธ์ฆ ์„œ๋ฒ„์— accessToken ์„ ์š”์ฒญ
  5. accessToken ๋ฐœ๊ธ‰

    • ์ธ์ฆ ์„œ๋ฒ„๋Š” ์š”์ฒญ์„ ๊ฒ€์ฆํ•œ ํ›„, ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ accessToken ์„ ๋ฐœ๊ธ‰
  6. ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„ ์š”์ฒญ

    • ํด๋ผ์ด์–ธํŠธ๋Š” accessToken ์„ ํฌํ•จํ•ด ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋ƒ„
    • ์„œ๋ฒ„๋Š” ํ† ํฐ์„ ๊ฒ€์ฆํ•˜์—ฌ ์š”์ฒญ๋œ ๋ฆฌ์†Œ์Šค ์ œ๊ณต

โœ”๏ธ ์˜ˆ์‹œ

๊ตฌ๊ธ€์€ ์›น ์‚ฌ์ดํŠธ ์‚ฌ์šฉ์ž๊ฐ€ '๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ' ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ๊ตฌ๊ธ€์—๊ฒŒ ์ „์†กํ•œ ๊ตฌ๊ธ€ ๊ณ„์ • ์ •๋ณด๊ฐ€ ์œ ํšจํ•œ์ง€ (๊ตฌ๊ธ€ ์•„์ด๋”” ๋ฐ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€) ๋ฅผ ํ™•์ธํ•œ๋‹ค.

์œ ํšจํ•˜๋‹ค๋ฉด ํ•ด๋‹นํ•˜๋Š” ๊ตฌ๊ธ€ ์œ ์ € ์ •๋ณด ์ค‘ ์ผ๋ถ€ (์œ ์ € ์ด๋ฆ„, ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋“ฑ) ๋ฅผ ๋‚ด ์›น ์‚ฌ์ดํŠธ์— ์ œ๊ณตํ•ด์ฃผ๋Š” '์ธ์ฆ' ๊ณผ์ •๋งŒ์„ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ๋ฐฉ์‹์ด๋‹ค!


โ” ์ธ์ฆ๊ณผ ์ธ๊ฐ€์˜ ์ฐจ์ด

์ธ์ฆ (Authentication)

์‚ฌ์šฉ์ž๊ฐ€ ๋ˆ„๊ตฌ์ธ์ง€ ํ™•์ธํ•˜๊ณ  ์ฆ๋ช…ํ•ด์ฃผ๋Š” ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ๊ฐ™์€ ๊ฒƒ

์ธ๊ฐ€ (Authorization)

์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒํ•œ

โœ… ์ธ์ฆ์ด ๋จผ์ € ์ด๋ฃจ์–ด์ง€๊ณ  ๊ทธ ๋‹ค์Œ ์ธ๊ฐ€๊ฐ€ ๋’ค๋”ฐ๋ฅด๊ฒŒ ๋จ


4-3. Spring Security ๊ตฌํ˜„ํ•˜๊ธฐ

์‚ฌ์šฉ์ž ์ด๋ฆ„ (๋‹‰๋„ค์ž„) ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋งŒ์„ ์ด์šฉํ•ด ๋กœ๊ทธ์ธํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด๋ณด์•˜๋‹ค.

1. SecurityConfig ์„ค์ •

  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์˜ ์ธ๊ฐ€ ๋ฐ ์„ค์ •์„ ๋‹ด๋‹นํ•˜๋Š” ํด๋ž˜์Šค
  • ์ธ์ฆ์„ ๊ด€๋ฆฌํ•˜๋Š” AuthenticationManager๋ฅผ ์„ค์ •
  • LoginFilter, JWTFilter, CustomLogoutFilter๋ฅผ ์‹œํ๋ฆฌํ‹ฐ ํ•„ํ„ฐ ์ฒด์ธ์— ์ถ”๊ฐ€ํ•ด์„œ ๊ฐ ํ•„ํ„ฐ๊ฐ€ ์ ์ ˆํ•œ ์‹œ์ ์— ๋™์ž‘ํ•˜๋„๋ก ๊ตฌ์„ฑ

โœ”๏ธ http.authorizeHttpRequests().requestMatchers(...)

  • ๊ฒฝ๋กœ๋ณ„ ์ธ๊ฐ€ ์ž‘์—… ๋‹ด๋‹น
  • ํŠน์ • url์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ ์„ค์ •
  • permitAll : ๋ชจ๋“  ๊ถŒํ•œ ํ—ˆ์šฉ
  • .anyRequest().authenticated()); : ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ (์ธ์ฆ ํ•„์š”)

โœ”๏ธ Stateless ์ƒํƒœ ์ง€์ •

http
        .sessionManagement((session) -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

JWT๋ฅผ ํ†ตํ•œ ์ธ์ฆ/์ธ๊ฐ€๋ฅผ ์œ„ํ•ด์„œ ์„ธ์…˜์„ STATELESS ์ƒํƒœ๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”!!

โœ”๏ธ csrf ๋น„ํ™œ์„ฑํ™”ํ•ด๋„ ๋˜๋Š” ์ด์œ ?

CSRF ๋Š” ์ฃผ๋กœ ์„ธ์…˜ ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๊ณต๊ฒฉ

  • JWT ์ธ์ฆ ๋ฐฉ์‹์—์„œ๋Š” ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์˜ ์„ธ์…˜ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜์ง€ ์•Š๊ณ , ๋งค ์š”์ฒญ๋งˆ๋‹ค ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ† ํฐ์„ ํฌํ•จํ•ด ์ธ์ฆ์„ ์ˆ˜ํ–‰
  • JWT๋Š” ๊ฐ ์š”์ฒญ์— ์ธ์ฆ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„ธ์…˜์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” Stateless ๋ฐฉ์‹ โ‡’ CSRF ๋ณดํ˜ธ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Œ

CORS

  • ์›น ๋ธŒ๋ผ์šฐ์ €๋Š” ๋ณด์•ˆ์ƒ์˜ ์ด์œ ๋กœ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ ๊ฐ„์˜ ๋ฆฌ์†Œ์Šค ์š”์ฒญ์„ ์ œํ•œํ•จ
  • CORS ์„ค์ •์„ ํ†ตํ•ด ํŠน์ • ๋„๋ฉ”์ธ์—์„œ์˜ ์š”์ฒญ ํ—ˆ์šฉ ๊ฐ€๋Šฅ

โœ”๏ธ CORS ์—๋Ÿฌ

์„œ๋กœ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐ„ API ํ˜ธ์ถœ ์ œํ•œ๋˜์–ด ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ

์˜ˆ) ๋ฐฑ์—”๋“œ์˜ 8080 ํฌํŠธ์™€ ํ”„๋ก ํŠธ์—”๋“œ์˜ 3000 ํฌํŠธ

โ‡’ ํฌํŠธ๊ฐ€ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋กœ ๋‹ค๋ฅธ ์ถœ์ฒ˜๋กœ ์ธ์‹๋˜์–ด CORS ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ

configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));

โœ… ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฐ์ดํ„ฐ ๋ณด๋‚ผ 3000๋ฒˆ๋Œ€ ํฌํŠธ ํ—ˆ์šฉ


2. ๋กœ๊ทธ์ธ ๋กœ์ง ๊ตฌํ˜„

1. LoginFilter

  • ์ปค์Šคํ…€ UsernamePasswordAuthentication ํ•„ํ„ฐ ์ž‘์„ฑ (์ƒ์†๋ฐ›์•„ ์‚ฌ์šฉ)
  • ๋กœ๊ทธ์ธ ์š”์ฒญ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•„ํ„ฐ โ‡’ ์•„์ด๋””, ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ์„ ์œ„ํ•œ ์ปค์Šคํ…€ ํ•„ํ„ฐ
  • AuthenticationManager ๋ฅผ ์ด์šฉํ•˜์—ฌ DB์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” ํšŒ์› ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒ€์ฆํ•  ๋กœ์ง ์ž‘์„ฑ
  • ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ JWT๋ฅผ ๋ฐ˜ํ™˜ํ•  success ํ•ธ๋“ค๋Ÿฌ ์ƒ์„ฑ
  • ์ปค์Šคํ…€ํ•œ ํ•„ํ„ฐ SecurityConfig์— ๋“ฑ๋กํ•ด์•ผ ํ•จ
  • refreshToken์€ DB์— ์ €์žฅํ•ด์„œ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•จ

2. CustomUserDetailsService

  • UserDetailsService ์ปค์Šคํ…€ํ•ด์„œ ๊ตฌํ˜„
  • DB์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ธฐ๋Šฅ

3. CustomUserDetails

  • UserDetails ์ปค์Šคํ…€ํ•ด์„œ ๊ตฌํ˜„
  • ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ด€๋ฆฌ
  • UserDetailsService ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋„˜๊ฒจ์ฃผ๋Š” DTO ์—ญํ• 
  • ์—ฌ๊ธฐ์„œ ์‹คํ–‰๋˜๋Š” getMemberId ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ถ”ํ›„ ์ปจํŠธ๋กค๋Ÿฌ์— ์‚ฌ์šฉ๋  ์‚ฌ์šฉ์ž ํ† ํฐ ์ •๋ณด ๊ฐ€์ ธ์˜ด

โญ ํšŒ์› ๊ฒ€์ฆ

  • UsernamePasswordAuthenticationFilter๊ฐ€ ํ˜ธ์ถœํ•œ AuthenticationManager ๋ฅผ ํ†ตํ•ด ์ง„ํ–‰

  • AuthenticationManager ๋Š” DB์—์„œ ์กฐํšŒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ UserDetailsService๋ฅผ ํ†ตํ•ด ๋ฐ›์•„์„œ ํšŒ์› ๊ฒ€์ฆ

4. JWTUtil

  • JWT์— ๊ด€ํ•œ ๋ฐœ๊ธ‰๊ณผ ๊ฒ€์ฆ์„ ๋‹ด๋‹นํ•˜๋Š” ํด๋ž˜์Šค
  • JWT๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š” ํ•ต์‹ฌ ๋กœ์ง!

5. JWTFilter

  • ๋“ค์–ด์˜ค๋Š” HTTP ์š”์ฒญ์˜ ํ—ค๋”์—์„œ JWT ์ถ”์ถœ ๋ฐ ์‚ฌ์šฉ์ž ์ธ์ฆ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ ์ง„ํ–‰

  • ์š”์ฒญ ํ—ค๋” Authorization ํ‚ค์— JWT๊ฐ€ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ, JWT๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  ๊ฐ•์ œ๋กœ SecurityContextHolder ์— ์„ธ์…˜์„ ์ƒ์„ฑ

    (์ด ์„ธ์…˜์€ STATELESS ์ƒํƒœ๋กœ ๊ด€๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ์š”์ฒญ์ด ๋๋‚˜๋ฉด ์†Œ๋ฉธ)

โญ ์•ก์„ธ์Šค & ๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ ๋ฐœ๊ธ‰

๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ Access/Refresh์— ํ•ด๋‹นํ•˜๋Š” ๋‹ค์ค‘ ํ† ํฐ ๋ฐœ๊ธ‰ โ‡’ ์ด 2๊ฐœ์˜ ํ† ํฐ์„ ๋ฐœ๊ธ‰

  • accessToken: ํ—ค๋”์— ๋ฐœ๊ธ‰ ํ›„ ํ”„๋ก ํŠธ์—์„œ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์ €์žฅ
  • refreshToken: ์ฟ ํ‚ค์— ๋ฐœ๊ธ‰

โฉ Postman ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ

/login : Spring Security์—์„œ ๊ธฐ๋ณธ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋กœ๊ทธ์ธ ์—”๋“œํฌ์ธํŠธ

/login POST ์š”์ฒญ์„ ํ†ตํ•ด ๋‹‰๋„ค์ž„๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด, ์‘๋‹ต ํ—ค๋”์— accessToken ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋œจ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, refreshToken ๋˜ํ•œ ์ฟ ํ‚ค์— ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฐœ๊ธ‰๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!


3. ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ๋กœ์ง ๊ตฌํ˜„

  1. ์„œ๋ฒ„ ์ธก JWTFilter์—์„œ accessToken์˜ ๋งŒ๋ฃŒ๋กœ ์ธํ•œ ํŠน์ •ํ•œ ์ƒํƒœ ์ฝ”๋“œ๊ฐ€ ํ”„๋ก ํŠธ์—”๋“œ์—๊ฒŒ ์‘๋‹ต
  2. ํ”„๋ก ํŠธ ์ธก์˜ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ์—์„œ accessToken ์žฌ๋ฐœ๊ธ‰์„ ์œ„ํ•œ refreshToken์„ ์„œ๋ฒ„ ์ธก์œผ๋กœ ์ „์†ก
  3. ์„œ๋ฒ„์—์„œ๋Š” refreshToken์„ ๋ฐ›์•„ ์ƒˆ๋กœ์šด accessToken์„ ์‘๋‹ต
  4. ์ด๋•Œ accessToken ๊ฐฑ์‹  ์‹œ refreshToken๋„ ํ•จ๊ป˜ ๊ฐฑ์‹  (โญ Refresh Rotate)

โญ Refresh Rotate๋ž€?

refreshToken์„ ๋ฐ›์•„ accessToken ๊ฐฑ์‹  ์‹œ refreshToken๋„ ํ•จ๊ป˜ ๊ฐฑ์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•

Rotate ๋˜๊ธฐ ์ด์ „์˜ ํ† ํฐ์„ ๊ฐ€์ง€๊ณ  ์„œ๋ฒ„์ธก์œผ๋กœ ๊ฐ€๋„ ์ธ์ฆ์ด ๋˜๋Š” ๋ฌธ์ œ ๋ฐœ์ƒ!

โœ… ์„œ๋ฒ„์ธก์—์„œ ๋ฐœ๊ธ‰ํ–ˆ๋˜ refreshToken๋“ค์„ ๊ธฐ์–ตํ•œ ๋’ค ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๋กœ์ง์„ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค!

(Rotate ์ด์ „์˜ refreshToken์€ ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•˜๋„๋ก,,)

  • refreshToken ๊ต์ฒด๋กœ ๋ณด์•ˆ์„ฑ ๊ฐ•ํ™”
  • ๋กœ๊ทธ์ธ ์ง€์†์‹œ๊ฐ„ ๊ธธ์–ด์ง

์ƒ๋ช…์ฃผ๊ธฐ๊ฐ€ ๊ธด refreshToken์€ ๋ฐœ๊ธ‰ ์‹œ ์„œ๋ฒ„์ธก ์ €์žฅ์†Œ์— ์ €์žฅํ•œ ํ›„ ์„œ๋ฒ„์— ๊ธฐ์–ต๋˜์–ด ์žˆ๋Š” refreshToken๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค (์„œ๋ฒ„์ธก ์ฃผ๋„๊ถŒ)

  1. ๋ฐœ๊ธ‰์‹œ : refreshToken์„ ์„œ๋ฒ„์ธก ์ €์žฅ์†Œ์— ์ €์žฅ

  2. ๊ฐฑ์‹ ์‹œ (Refresh Rotate) : ๊ธฐ์กด refreshToken์„ ์‚ญ์ œํ•˜๊ณ  ์ƒˆ๋กœ ๋ฐœ๊ธ‰ํ•œ refreshToken์„ ์ƒˆ๋กœ ์ €์žฅ

โœ”๏ธ ํ† ํฐ ์ €์žฅ์†Œ

RDB ๋˜๋Š” Redis์™€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํ†ตํ•ด refreshToken์„ ์ €์žฅํ•œ๋‹ค.

์ด๋•Œ Redis์˜ ๊ฒฝ์šฐ, TTL ์„ค์ •์„ ํ†ตํ•ด ์ƒ๋ช…์ฃผ๊ธฐ๊ฐ€ ๋๋‚œ ํ† ํฐ์„ ์ž๋™์œผ๋กœ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.


4. ๋กœ๊ทธ์•„์›ƒ ๋กœ์ง ๊ตฌํ˜„

CustomLogoutFilter ๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ์•„์›ƒ ๋กœ์ง์„ ๊ตฌํ˜„ํ•œ๋‹ค.

ํ”„๋ก ํŠธ์—”๋“œ์ธก

๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์กด์žฌํ•˜๋Š” accessToken ์‚ญ์ œ ๋ฐ ์„œ๋ฒ„์ธก ๋กœ๊ทธ์•„์›ƒ ๊ฒฝ๋กœ๋กœ refreshToken ์ „์†ก

๋ฐฑ์—”๋“œ์ธก

  • refreshToken์„ ๋ฐ›์•„ Cookie ์ดˆ๊ธฐํ™” (NULL) ํ›„, Refresh DB์—์„œ ํ•ด๋‹น refreshToken ์‚ญ์ œ
  • ์„ธ์…˜์„ ๋ฌดํšจํ™”ํ•˜๊ณ , ์ธ์ฆ ์ •๋ณด๋ฅผ ์ œ๊ฑฐ

(nickname ๊ธฐ๋ฐ˜์œผ๋กœ ๋ชจ๋“  refreshToken ์‚ญ์ œํ•˜๋Š” ๋กœ์ง ๊ตฌํ˜„)

โฉ Postman ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ

/logout : Spring Security์—์„œ ๊ธฐ๋ณธ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋กœ๊ทธ์•„์›ƒ ์—”๋“œํฌ์ธํŠธ

/logout POST ์š”์ฒญ์„ ๋ณด๋‚ผ ์‹œ, ์ฟ ํ‚ค์— ์žˆ๋˜ refreshToken ๊ฐ’์ด ์‚ฌ๋ผ์ง€๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!


5. ํ† ํฐ์ด ํ•„์š”ํ•œ API ๊ตฌํ˜„

๊ธฐ์กด ์ฝ”๋“œ

@Operation(summary = "๊ฒŒ์‹œ๊ธ€ ์ „์ฒด ์กฐํšŒ", description = "๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ์ „์ฒด ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ•˜๋Š” API")
@GetMapping("/my/{memberId}")
public CommonResponse<List<PostResponseDto>> getAllPosts(@PathVariable Long memberId) {

    return new CommonResponse<>(ResponseCode.SUCCESS, postService.getAllPosts(memberId));
}

โ™ป๏ธ ํ† ํฐ๊ฐ’ ์ ์šฉํ•œ ์ฝ”๋“œ

@Operation(summary = "๊ฒŒ์‹œ๊ธ€ ์ „์ฒด ์กฐํšŒ", description = "๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ์ „์ฒด ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ•˜๋Š” API")
@GetMapping("/my")
public CommonResponse<List<PostResponseDto>> getAllPosts(@AuthenticationPrincipal CustomUserDetails userDetails) {

    return new CommonResponse<>(ResponseCode.SUCCESS, postService.getAllPosts(userDetails.getMemberId()));
}

@AuthenticationPrincipal

  • Spring Security์—์„œ ํ˜„์žฌ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์— ์ง์ ‘ ์ฃผ์ž…ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜
  • CustomUserDetails ๋ฅผ ์ฃผ์ž…ํ•จ์œผ๋กœ์จ, memberId ๋ฅผ ๊ฐ€์ ธ์™€ ์คŒ

โœ”๏ธ ๋กœ๊ทธ์ธ ์‹œ ์‘๋‹ต ํ—ค๋”์— ๊ธฐ๋ก๋˜๋Š” accessToken์„ swagger์˜ Authorize ๊ฐ’์— ๋„ฃ์–ด์„œ ์‚ฌ์šฉ์ž ์ธ์ฆ์„ ํ•ด์•ผ ํ•œ๋‹ค!


โœ”๏ธ ์ธ์ฆ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋˜์—ˆ๋‹ค๋ฉด, memberId ๊ฐ’์„ ์ž…๋ ฅํ•˜์ง€ ์•Š์•„๋„ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์‚ฌ์šฉ์ž ํ† ํฐ์ด ํ•„์š”ํ•œ API ์š”์ฒญ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ“ข ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… < Swagger 403 ์˜ค๋ฅ˜ ๋ฌธ์ œ >

1. ๋ฌธ์ œ์ 

ํ† ํฐ์ด ํ•„์š”ํ•œ ๋ชจ๋“  API ์š”์ฒญ ํ…Œ์ŠคํŠธ ์‹œ, 403 Forbidden ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

403 Forbidden : ์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†๋Š” ๊ฒฝ์šฐ

Swagger์—์„œ 403 Forbidden ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ฃผ์š” ์›์ธ์—๋Š” ๋ฌด์—‡์ด ์žˆ์„๊นŒ?

์ฃผ๋กœ ์ธ์ฆ ๋˜๋Š” ์ธ๊ฐ€์— ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ๋ผ๊ณ  ํ•œ๋‹ค.


1. JWT ํ† ํฐ์ด ๋ˆ„๋ฝ๋˜๊ฑฐ๋‚˜ ์ž˜๋ชป๋œ ๊ฒฝ์šฐ

  • ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ, Authorization ํ—ค๋”์— JWT ํ† ํฐ์„ ํฌํ•จํ•˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ์ž˜๋ชป๋œ ํ˜•์‹์œผ๋กœ ํฌํ•จํ•œ ๊ฒฝ์šฐ

๐Ÿค” Swagger์˜ Authorize ๋ฒ„ํŠผ์„ ํ†ตํ•ด, /login ์‹œ ์‘๋‹ต ํ—ค๋”๋กœ๋ถ€ํ„ฐ ์ „ํ•ด์ง„ accessToken ๊ฐ’์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋„˜๊ฒผ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค!


2. ํ† ํฐ์ด ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ

  • JWT ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜๋ฉด ์ธ์ฆ์ด ์‹คํŒจํ•จ

๐Ÿค” ๋ฐฉ๊ธˆ ๋กœ๊ทธ์ธํ•ด์„œ ๋ฐ›์€ accessToken ๊ฐ’์ด๊ธฐ ๋•Œ๋ฌธ์— ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋ฆฌ๊ฐ€ ์—†๋‹ค!


3. Spring Security ์„ค์ •์—์„œ ๊ฒฝ๋กœ ์ ‘๊ทผ ๊ถŒํ•œ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ

  • ํŠน์ • ๊ฒฝ๋กœ์— ๋Œ€ํ•ด ๊ถŒํ•œ์ด ํ•„์š”ํ•˜์ง€๋งŒ, ์š”์ฒญ์„ ๋ณด๋‚ธ ์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ์ด ๋ถ€์กฑํ•œ ๊ฒฝ์šฐ
http
        .authorizeHttpRequests((auth) -> auth
        .requestMatchers("/login", "/", "/api/auth/signup", "/api/auth/reissue", "/swagger-ui.html", "/swagger-ui/**","/v3/api-docs/**").permitAll()
                        .requestMatchers("/api/auth/admin").hasRole("ADMIN")    // ADMIN ๊ถŒํ•œ ์„ค์ •
                        .anyRequest().authenticated()   // ๋”ฐ๋กœ ๊ถŒํ•œ ์„ค์ • ์—†์ด ์ธ์ฆ๋งŒ ์ด๋ฃจ์–ด์ง€๋ฉด ์ ‘๊ทผ ๊ฐ€๋Šฅ
                );

๐Ÿค” admin ๊ฒฝ๋กœ๋ฅผ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€ API ๊ฒฝ๋กœ์˜ ๊ฒฝ์šฐ ๋”ฐ๋กœ ๊ถŒํ•œ ์„ค์ •์„ ํ•˜์ง€ ์•Š์•˜๋‹ค!


4. CORS ์„ค์ • ๋ฌธ์ œ

  • ํด๋ผ์ด์–ธํŠธ (Swagger UI) ์™€ ์„œ๋ฒ„ ๋„๋ฉ”์ธ์ด ๋‹ค๋ฅผ ๋•Œ, CORS ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์œผ๋ฉด ์„œ๋ฒ„๊ฐ€ ์š”์ฒญ์„ ์ฐจ๋‹จ

๐Ÿค” ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ localhost:8080์„ ํ†ตํ•ด Swagger๋กœ ํ…Œ์ŠคํŠธํ•œ ๊ฒฝ์šฐ์ด๋ฏ€๋กœ, ๋„๋ฉ”์ธ์ด ๋‹ฌ๋ผ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜์ธ CORS ์—๋Ÿฌ๊ฐ€ ์›์ธ์ผ ๊ฐ€๋Šฅ์„ฑ์€ ์ ๋‹ค!


ํ•ด๋‹น ๊ฒฝ์šฐ๋“ค์ด ๋ชจ๋‘ ์„ฑ๋ฆฝํ•˜์ง€ ์•Š๋Š”๋ฐ, ๋„๋Œ€์ฒด ์˜ค๋ฅ˜์˜ ์›์ธ์€ ๋ฌด์—‡์ผ๊นŒ??


2. ๋ฌธ์ œ์˜ ์›์ธ

/** [ JWTFilter ์ฝ”๋“œ ] **/

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            
    // ํ—ค๋”์—์„œ accessํ‚ค์— ๋‹ด๊ธด ํ† ํฐ์„ ๊บผ๋ƒ„
    String accessToken = request.getHeader("access");

    
/** [ LoginFilter ์ฝ”๋“œ ] **/

@Override 
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException {

        ( ์ƒ๋žต )   
        
    // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ๋ฐœ๊ธ‰๋˜๋Š” ํ† ํฐ์— ๋Œ€ํ•œ ์‘๋‹ต ์„ค์ •
    response.setHeader("access", access);

์ฒ˜์Œ์—๋Š” ๋กœ๊ทธ์ธ์„ ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ ๋ฐœ๊ธ‰๋˜๋Š” accessToken์— ๋Œ€ํ•œ ํ—ค๋” ์ด๋ฆ„์„ (ํ™•์ธํ•˜๊ธฐ ์‰ฌ์šฐ๋ผ๊ณ ) access ๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ์„ค์ •์„ ์ง„ํ–‰ํ•˜์˜€๋‹ค.

๋‚ด๊ฐ€ ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹น์—ฐํ•˜๊ฒŒ๋„ ํ—ค๋”์—์„œ accessToken์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒฝ์šฐ, access ํ‚ค์— ๋‹ด๊ธด ํ† ํฐ์„ ๊บผ๋‚ด๋Š” ๋กœ์ง์œผ๋กœ getHeader ๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฒƒ์ด์—ˆ๋‹ค.

โ— ์ด๊ฒŒ ๋ฐ”๋กœ ๋ฌธ์ œ์˜ ์›์ธ์ด์—ˆ๋‹ค โ—

๋‹ค์‹œ swagger์˜ ์‘๋‹ต์„ ์‚ดํŽด๋ณด์ž.

์Šคํฌ๋ฆฐ์ƒท 2024-11-08 152843

Authorization : Bearer <token> ํ˜•ํƒœ๋กœ accessToken ๊ฐ’์ด ๋“ค์–ด์˜ค๊ณ  ์žˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!

์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ ๋‚˜์˜ ์ฝ”๋“œ๋Š” ํ—ค๋”์˜ access ํ‚ค์˜ ํ† ํฐ์„ ๊บผ๋‚ด์ค˜! ๋ผ๊ณ  ์š”์ฒญํ•˜๊ณ  ์žˆ์œผ๋‹ˆ swagger์—์„œ๋Š” ํ† ํฐ์— ๋Œ€ํ•œ ์ธ์‹ ์ž์ฒด๋ฅผ ํ•˜์ง€ ๋ชปํ•œ ๊ฒƒ์ด์—ˆ๋‹ค.

โญ Authorization : Bearer <token>

  • HTTP ํ‘œ์ค€๊ณผ Spring Security์—์„œ ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•˜๋Š” ํ‘œ์ค€ ๋ฐฉ์‹
  • ๋Œ€๋ถ€๋ถ„์˜ ํด๋ผ์ด์–ธํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (์˜ˆ: Axios, Postman, Swagger ๋“ฑ) ์™€ ๋ธŒ๋ผ์šฐ์ €์˜ ์ธ์ฆ ํ† ํฐ ๊ด€๋ฆฌ ๋ฐฉ์‹์ด Authorization ํ—ค๋”์— ์˜์กดํ•œ๋‹ค๊ณ  ํ•จ

Authorization ํ—ค๋” ๋Œ€์‹  ๋‹ค๋ฅธ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜๋ฉด, ์ž๋™์œผ๋กœ Bearer ํ† ํฐ์„ ์ธ์‹ํ•˜์ง€ ๋ชปํ•˜๊ณ  ์ธ์ฆ ์ฒ˜๋ฆฌ๊ฐ€ ๋ˆ„๋ฝ๋  ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ!!


3. ํ•ด๊ฒฐ์ฑ…

/** [ JWTFilter ์ฝ”๋“œ ] **/

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    // Authorization ํ—ค๋”๊ฐ’ ์ถ”์ถœ
    String header = request.getHeader("Authorization");
    if (header == null || !header.startsWith("Bearer ")) {
        filterChain.doFilter(request, response);
        return;
    }

    // "Bearer " ์ ‘๋‘์‚ฌ ์ œ๊ฑฐ ํ›„ accessToken๋งŒ ์ถ”์ถœ
    String token = header.substring(7);

    
/** [ LoginFilter ์ฝ”๋“œ ] **/

@Override 
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException {

        ( ์ƒ๋žต )   
        
    // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ๋ฐœ๊ธ‰๋˜๋Š” ํ† ํฐ์— ๋Œ€ํ•œ ์‘๋‹ต ์„ค์ • -> ํ‘œ์ค€ ๋ฐฉ์‹์œผ๋กœ ์ˆ˜์ •
    response.setHeader("Authorization", "Bearer " + access);

< ์ตœ์ข… ๊ฒฐ๊ณผ >



5. Docker

5-1. Docker ์ดํ•ดํ•˜๊ธฐ

๐Ÿ‹ Docker ๋ž€?

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2021-08-19 แ„‹แ…ฉแ„’แ…ฎ 6 12 36
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹ ์†ํ•˜๊ฒŒ ๊ตฌ์ถ•, ํ…Œ์ŠคํŠธ ๋ฐ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋Š” ์†Œํ”„ํŠธ์›จ์–ด ํ”Œ๋žซํผ

  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ๊ทธ์— ํ•„์š”ํ•œ ๋ชจ๋“  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, ์ข…์†์„ฑ, ์„ค์ • ํŒŒ์ผ ๋“ฑ์„ ์ปจํ…Œ์ด๋„ˆ๋ผ๋Š” ๋…๋ฆฝ๋œ ํ™˜๊ฒฝ์— ํŒจํ‚ค์ง•ํ•˜์—ฌ, ์–ด๋””์„œ๋“  ์ผ๊ด€๋˜๊ฒŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์คŒ (์šด์˜ํ™˜๊ฒฝ ์˜์กดX)

  • docker hub์—์„œ image๋ฅผ pull ํ•˜๊ณ , image ๋ฅผ run ํ•˜๋ฉด container๊ฐ€ ์‹คํ–‰

  • ํ™œ์šฉ ์˜ˆ์‹œ ) ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ, ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ์ถ•, CI/CD ํŒŒ์ดํ”„๋ผ์ธ


๐Ÿ‹ Docker์—์„œ์˜ ์ด๋ฏธ์ง€๋ž€?

  • ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ํ…œํ”Œ๋ฆฟ

  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ(์šด์˜ ์ฒด์ œ, ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์ผ ๋“ฑ)์„ ํฌํ•จํ•˜๋Š” ์ฝ๊ธฐ ์ „์šฉ ํŒŒ์ผ ์‹œ์Šคํ…œ

    • ๋„์ปค ์ด๋ฏธ์ง€์—๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰์— ํ•„์š”ํ•œ ์†Œํ”„ํŠธ์›จ์–ด์™€ ํ™˜๊ฒฝ์ด ๋ชจ๋‘ ๋“ค์–ด ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ์–ด๋””์„œ๋“  ๋™์ผํ•œ ํ™˜๊ฒฝ์—์„œ ์ผ๊ด€๋˜๊ฒŒ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Œ
  • ์˜ˆ์‹œ) MySQL ์ด๋ฏธ์ง€

    • mysql:5.7๊ณผ ๊ฐ™์€ Docker ์ด๋ฏธ์ง€๋Š” MySQL ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•˜๋Š” ํ…œํ”Œ๋ฆฟ

    • docker pull mysql:5.7 : 'MySQL ์ด๋ฏธ์ง€๋ฅผ Docker Hub ์—์„œ ๋‹ค์šด๋ฐ›๋Š”๋‹ค' ๋Š” ์˜๋ฏธ


๐Ÿ‹ Docker์—์„œ์˜ ์ปจํ…Œ์ด๋„ˆ๋ž€?

  • ์ด๋ฏธ์ง€๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋งŒ๋“  ์‹ค์ œ ์‹คํ–‰ ์ค‘์ธ ํ™˜๊ฒฝ

  • ์ปจํ…Œ์ด๋„ˆ๋Š” ๋…๋ฆฝ์ ์ด๊ณ  ๊ฒฉ๋ฆฌ๋œ ํ™˜๊ฒฝ์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰

  • ์˜ˆ์‹œ) MySQL ์ปจํ…Œ์ด๋„ˆ

    • mysql:5.7 ์ด๋ฏธ์ง€๋ฅผ ์‹คํ–‰ํ•˜์—ฌ MySQL ์„œ๋ฒ„๋ฅผ ์ž‘๋™์‹œํ‚ค๋Š” ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ƒ์„ฑ

    • docker run -d --name mysql-container mysql:5.7 : mysql:5.7 ์ด๋ฏธ์ง€๋ฅผ ์‹คํ–‰ํ•˜์—ฌ mysql-container์ด๋ผ๋Š” ์ด๋ฆ„์˜ MySQL ์ปจํ…Œ์ด๋„ˆ ๋งŒ๋“ค๊ธฐ


๐Ÿ‹ Docker์—์„œ์˜ ๋„คํฌ์›Œํฌ๋ž€?

  • ์—ฌ๋Ÿฌ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์„œ๋กœ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” ๊ฐ€์ƒ ๋„คํŠธ์›Œํฌ

  • ๊ธฐ๋ณธ์ ์œผ๋กœ Docker ์ปจํ…Œ์ด๋„ˆ๋Š” ๋ธŒ๋ฆฌ์ง€ ๋„คํŠธ์›Œํฌ์— ์—ฐ๊ฒฐ โ‡’ ๋™์ผํ•œ Docker ํ˜ธ์ŠคํŠธ ๋‚ด์—์„œ ์ปจํ…Œ์ด๋„ˆ๋“ค์ด ์„œ๋กœ ํ†ต์‹  ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์คŒ

docker network create my-network
docker run -d --name container1 --network my-network my-image
docker run -d --name container2 --network my-network my-image

โœ”๏ธ my-network๋ผ๋Š” ๋„คํŠธ์›Œํฌ๋ฅผ ๋งŒ๋“ค๊ณ , container1๊ณผ container2๋ผ๋Š” ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๊ทธ ๋„คํŠธ์›Œํฌ์— ์—ฐ๊ฒฐ

โœ”๏ธ ๋‘ ์ปจํ…Œ์ด๋„ˆ๋Š” ์„œ๋กœ ํ†ต์‹  ๊ฐ€๋Šฅ!


์ตœ์ข… ์ •๋ฆฌ

  • ๋„์ปค ํŒŒ์ผ (Dockerfile) : ์ด๋ฏธ์ง€๋ฅผ ๋งŒ๋“œ๋Š” ์„ค์ • ํŒŒ์ผ (๋ ˆ์‹œํ”ผ)

  • ์ด๋ฏธ์ง€ (Image): ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•˜๋Š” ํ…œํ”Œ๋ฆฟ (๋ ˆ์‹œํ”ผ์˜ ์ค€๋น„๋ฌผ)

  • ์ปจํ…Œ์ด๋„ˆ (Container): ์ด๋ฏธ์ง€๋ฅผ ์‹คํ–‰ํ•œ ์‹ค์ œ ์ธ์Šคํ„ด์Šค (์š”๋ฆฌ)

  • ๋„คํŠธ์›Œํฌ (Network): ์—ฌ๋Ÿฌ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์„œ๋กœ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” ๊ฐ€์ƒ ๋„คํŠธ์›Œํฌ (ํ…Œ์ด๋ธ” ์—ฐ๊ฒฐ)

๐Ÿ‘‰ ๋„์ปค ์ด๋ฏธ์ง€๋Š” ์ค€๋น„๋œ ์ƒํƒœ์ด๊ณ , ๋„์ปค ์ปจํ…Œ์ด๋„ˆ๋Š” ์ด ์ค€๋น„๋œ ์ด๋ฏธ์ง€๋ฅผ ์‹ค์ œ๋กœ ์‹คํ–‰ํ•œ ๊ฒฐ๊ณผ


5-2. Docker ๊ธฐ๋ฐ˜ ์Šคํ”„๋ง๋ถ€ํŠธ ๋นŒ๋“œํ•˜๊ธฐ

1. Dockerfile

Gradle ํƒญ์—์„œ Tasks-build-bootJar ์‹คํ–‰ โ†’ build/libs ๊ฒฝ๋กœ์— jar ํŒŒ์ผ ์ƒ์„ฑ โ†’ Dockerfile ์„ค์ •

  • Docker ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ • ํŒŒ์ผ

  • 1๊ฐœ์˜ ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑํ•˜๋Š” ํŒŒ์ผ

  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ, ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, ํ™˜๊ฒฝ ๋ณ€์ˆ˜, ์„ค์ • ํŒŒ์ผ ๋“ฑ์„ ํฌํ•จํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์‹คํ–‰ ํ™˜๊ฒฝ์„ ๋‚˜ํƒ€๋ƒ„


2. docker-compose.yml

  • MySQL๊ณผ Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ปจํ…Œ์ด๋„ˆํ™”ํ•˜์—ฌ ์‹คํ–‰ํ•˜๋Š” ์„ค์ •

  • ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์„œ๋น„์Šค (์ปจํ…Œ์ด๋„ˆ) ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ํŒŒ์ผ

  • ๋‘ ์„œ๋น„์Šค๋ฅผ ๋™์ผํ•œ ๋„คํŠธ์›Œํฌ์— ์—ฐ๊ฒฐํ•˜์—ฌ ์„œ๋กœ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๊ฒŒ๋” ํ•ด์คŒ

  • depends_on๊ณผ healthcheck๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ application ์„œ๋น„์Šค๊ฐ€ database ์„œ๋น„์Šค๊ฐ€ ์ค€๋น„๋œ ํ›„์— ์‹œ์ž‘๋˜๋„๋ก ๋ณด์žฅ

version: "3"

services:
  database:
    container_name: instagram
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: testdb
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      TZ: 'Asia/Seoul'
    ports:
      - "3306:3306"
    command:
      - "--character-set-server=utf8mb4"
      - "--collation-server=utf8mb4_unicode_ci"
    networks:
      - network
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -p${DB_PASSWORD} --silent"]
      interval: 30s
      retries: 5

  application:
    container_name: main-server
    build:
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      SPRING_DATASOURCE_URL: ${DB_URL}
      SPRING_DATASOURCE_USERNAME: ${DB_USERNAME}
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
    depends_on:
      database:
        condition: service_healthy
    networks:
      - network
    env_file:
      - .env

networks:
  network:
    driver: bridge

โญ ์ฝ”๋“œ ํ•ด์„

services :

  • Compose ํŒŒ์ผ ๋‚ด์—์„œ ๊ฐ๊ฐ์˜ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ •์˜ํ•˜๋Š” ๋ถ€๋ถ„
  • ๊ฐ ์„œ๋น„์Šค๋Š” ๊ฐœ๋ณ„ ์ปจํ…Œ์ด๋„ˆ๋กœ ์‹คํ–‰๋˜๋ฉฐ, database, application๊ณผ ๊ฐ™์€ ์ด๋ฆ„์„ ๊ฐ€์ง„ ์„œ๋น„์Šค๋“ค์„ ์ •์˜

1. database ์„œ๋น„์Šค

  database:
    container_name: instagram
  • database ์„œ๋น„์Šค์— ๋Œ€ํ•œ ์ปจํ…Œ์ด๋„ˆ ์ด๋ฆ„์„ instagram์œผ๋กœ ์„ค์ •

โœ…MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‹คํ–‰ํ•˜๋Š” Docker ์ปจํ…Œ์ด๋„ˆ instagram ์ƒ์„ฑ!


image: mysql:8.0

  • ์‚ฌ์šฉํ•  Docker ์ด๋ฏธ์ง€๋ฅผ ์ง€์ •
  • ํ•ด๋‹น ์ด๋ฏธ์ง€๋Š” MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํฌํ•จ

environment:
  MYSQL_DATABASE: testdb
  MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
  TZ: 'Asia/Seoul'
  • MYSQL_DATABASE
    • MySQL์ด ์ฒ˜์Œ ์‹คํ–‰๋  ๋•Œ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ด๋ฆ„์„ ์ง€์ •
    • ์—ฌ๊ธฐ์„œ๋Š” testdb๋ผ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์ƒ์„ฑ (๋‚ด MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ช…๊ณผ ์ผ์น˜)

โ—์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปจํ…Œ์ด๋„ˆ๋Š” ๋™์ผํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ด๋ฆ„๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•จ


ports:
  - "3306:3306"
  • ํฌํŠธ ๋งคํ•‘ โ‡’ ๋กœ์ปฌ ์‹œ์Šคํ…œ์—์„œ ์ปจํ…Œ์ด๋„ˆ ๋‚ด MySQL์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค!
  • Docker ์ปจํ…Œ์ด๋„ˆ ๋‚ด์˜ MySQL ์„œ๋น„์Šค๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 3306 ํฌํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์ด ํฌํŠธ๋ฅผ ํ˜ธ์ŠคํŠธ ์‹œ์Šคํ…œ์˜ 3306 ํฌํŠธ์™€ ์—ฐ๊ฒฐ
  • 3306:3306 ํ˜•์‹์€ ํ˜ธ์ŠคํŠธ(๋กœ์ปฌ) ํฌํŠธ์™€ ์ปจํ…Œ์ด๋„ˆ ํฌํŠธ๋ฅผ ๋งคํ•‘ํ•˜๋Š” ๋ฐฉ์‹

command:
  - "--character-set-server=utf8mb4"
  - "--collation-server=utf8mb4_unicode_ci"
  • MySQL ์ปค๋งจ๋“œ ์˜ต์…˜
  • ์ด๋ชจ์ง€๋‚˜ ๋‹ค๊ตญ์–ด ๋ฌธ์ž๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ ๋ฌธ์ œ๊ฐ€ ์—†์œผ๋ฉฐ, MySQL์—์„œ ๋” ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” UTF-8 ๋ฌธ์ž ์ง‘ํ•ฉ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ

networks:
  - network
  • ์ด ์„œ๋น„์Šค๊ฐ€ ์—ฐ๊ฒฐ๋  ๋„คํŠธ์›Œํฌ๋ฅผ ์ง€์ •
  • ์—ฌ๊ธฐ์„œ๋Š” network๋ผ๋Š” ์ด๋ฆ„์˜ ์‚ฌ์šฉ์ž ์ •์˜ ๋„คํŠธ์›Œํฌ์— ์ด ์„œ๋น„์Šค๊ฐ€ ์—ฐ๊ฒฐ๋จ

healthcheck:
  test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -p${DB_PASSWORD} --silent"]
  interval: 30s
  retries: 5

healthcheck๋Š” ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ค์ •ํ•˜๋Š” ์—ญํ• 

  • application ์„œ๋น„์Šค๊ฐ€ database ์„œ๋น„์Šค๊ฐ€ ์ค€๋น„๋˜์—ˆ์„ ๋•Œ๋งŒ ์‹คํ–‰๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์Œ

  • test: mysqladmin ping ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ MySQL ์„œ๋ฒ„๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ

    โœ”๏ธ -h 127.0.0.1์€ MySQL ์„œ๋ฒ„์˜ ํ˜ธ์ŠคํŠธ๋ฅผ ์ง€์ •

    โœ”๏ธ -p${DB_PASSWORD}๋Š” MySQL์˜ root ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ „๋‹ฌ

  • interval: healthcheck๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฐ„๊ฒฉ

  • retries: healthcheck ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์„ค์ •


2. application ์„œ๋น„์Šค

application:
  container_name: main-server
  • ์ด ์„œ๋น„์Šค์˜ ์ปจํ…Œ์ด๋„ˆ ์ด๋ฆ„์„ main-server๋กœ ์„ค์ •

build:
  dockerfile: Dockerfile
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋“œํ•  ๋•Œ ์‚ฌ์šฉํ•  Dockerfile์„ ์ง€์ •

    โ‡’ ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ Dockerfile์„ ์‚ฌ์šฉํ•˜์—ฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋“œํ•จ

  • Dockerfile : ๋‚ด ํ˜„์žฌ Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ Docker ์ด๋ฏธ์ง€๋กœ ๋งŒ๋“  ๊ฒƒ


ports:
  - "8080:8080"
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‚ฌ์šฉํ•˜๋Š” ํฌํŠธ๋ฅผ ํ˜ธ์ŠคํŠธ์™€ ์—ฐ๊ฒฐ
  • 8080 ํฌํŠธ๋ฅผ ๋งคํ•‘ํ•˜์—ฌ, ํ˜ธ์ŠคํŠธ์˜ 8080 ํฌํŠธ์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ

environment:
  SPRING_DATASOURCE_URL: ${DB_URL}
  SPRING_DATASOURCE_USERNAME: ${DB_USERNAME}
  SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
  • Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •
  • DB_URL = jdbc:mysql://instagram:3306/testdb?useSSL=false&serverTimezone=Asia/Seoul

โญ ์—ฌ๊ธฐ์„œ database ์ปจํ…Œ์ด๋„ˆ๋ช…์ธ instagram ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Œ์„ ์ฃผ๋ชฉํ•˜์ž!

application ์„œ๋น„์Šค๋Š” database ์„œ๋น„์Šค์— ์ ‘๊ทผํ•  ๋•Œ instagram์„ ํ˜ธ์ŠคํŠธ ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉํ•˜์—ฌ MySQL์— ์—ฐ๊ฒฐํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค!!

โ— ํ•ด๋‹น ๊ฐ’๋“ค์€ ์›๋ž˜ ์‚ฌ์šฉ ์ค‘์ธ ์‹ค์ œ MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์ •๋ณด์™€ ๋™์ผํ•˜๊ฒŒ ์„ค์ •ํ•ด์•ผ ํ•จ


depends_on:
  database:
    condition: service_healthy
  • depends_on
    • application ์„œ๋น„์Šค๊ฐ€ ์‹œ์ž‘๋˜๊ธฐ ์ „์— database ์„œ๋น„์Šค๊ฐ€ ์‹คํ–‰ ์ค‘์ด์–ด์•ผ ํ•˜๋ฉฐ, ๋‹จ์ˆœํžˆ ์‹คํ–‰ ์ˆœ์„œ๋งŒ ๋ณด์žฅ
  • condition: service_healthy
    • application ์„œ๋น„์Šค๊ฐ€ ์‹œ์ž‘๋˜๊ธฐ ์ „์— database ์„œ๋น„์Šค๊ฐ€ ๊ฑด๊ฐ•ํ•œ ์ƒํƒœ(์ฆ‰, healthcheck๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ํ†ต๊ณผํ•œ ์ƒํƒœ)์ผ ๋•Œ๋งŒ ์‹คํ–‰
    • database ์„œ๋น„์Šค์˜ healthcheck๊ฐ€ ์„ฑ๊ณตํ•  ๋•Œ๊นŒ์ง€ application ์„œ๋น„์Šค์˜ ์‹œ์ž‘์„ ์ง€์—ฐ์‹œํ‚ด

networks:
  - network
  • application ์„œ๋น„์Šค๊ฐ€ network ๋„คํŠธ์›Œํฌ์— ์—ฐ๊ฒฐ๋˜๋„๋ก ์„ค์ •
  • database ์„œ๋น„์Šค์™€ ๋™์ผํ•œ ๋„คํŠธ์›Œํฌ์— ์†ํ•˜๊ฒŒ ๋˜์–ด ์„œ๋กœ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ์Œ

3. network

networks:
  network:
    driver: bridge
  • ์‚ฌ์šฉ์ž ์ •์˜ ๋„คํŠธ์›Œํฌ๋ฅผ ์ •์˜
  • bridge๋Š” ๊ธฐ๋ณธ Docker ๋„คํŠธ์›Œํฌ ๋“œ๋ผ์ด๋ฒ„๋กœ, ์ด ๋„คํŠธ์›Œํฌ์— ์—ฐ๊ฒฐ๋œ ์ปจํ…Œ์ด๋„ˆ๋Š” ์„œ๋กœ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ์Œ

โ— ์ปจํ…Œ์ด๋„ˆ ๊ฐ„ ์—ฐ๊ฒฐ์„ ์œ„ํ•ด ๋„คํŠธ์›Œํฌ๋Š” ํ•„์ˆ˜!


๐Ÿ“ข ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… - 1 < MySQL ์„œ๋ฒ„์™€์˜ ์—ฐ๊ฒฐ ์‹คํŒจ >

1. ๋ฌธ์ œ์ 

  1. connection refused ๋ฌธ์ œ
  2. Communications link failure ๋ฌธ์ œ

๋‘ ์—๋Ÿฌ์˜ ๊ณตํ†ต ์›์ธ:

  • MySQL ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์Œ ๋˜๋Š” ์—ฐ๊ฒฐ ์ค€๋น„๊ฐ€ ๋˜์ง€ ์•Š์Œ
  • ์ž˜๋ชป๋œ ์—ฐ๊ฒฐ ์ •๋ณด (ํ˜ธ์ŠคํŠธ, ํฌํŠธ, ์‚ฌ์šฉ์ž๋ช…, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋“ฑ)

๋‘ ๋ฌธ์ œ ๋ชจ๋‘ MySQL ์„œ๋ฒ„์™€์˜ ์—ฐ๊ฒฐ์ด ์‹คํŒจํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋ผ๊ณ  ํ•œ๋‹ค..


2. ๋ฐœ์ƒ ์›์ธ

โœ… ์ฒซ๋ฒˆ์งธ ์›์ธ์œผ๋กœ๋Š”,

ํ˜„์žฌ ๋‚˜์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋น„๋ฐ€๋ฒˆํ˜ธ์— $ ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”๋ฐ, yml์— ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ์ž‘์„ฑํ•˜๋‹ค๋ณด๋‹ˆ ํ•ด๋‹น ๋ฌธ์ž๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ•ด ์—ฐ๊ฒฐ ๊ฑฐ๋ถ€ ํ˜„์ƒ์ด ๋ฐœ์ƒํ•˜์˜€๋‹ค.

โœ… ๋‘๋ฒˆ์งธ ์›์ธ์œผ๋กœ๋Š”,

depends_on ๋งŒ ์‚ฌ์šฉํ•˜๊ณ  Healthcheck ๋Š” ํ•ด์ฃผ์ง€ ์•Š์•˜๋‹ค.

application ์„œ๋น„์Šค๊ฐ€ database ์„œ๋น„์Šค์— ์—ฐ๊ฒฐํ•˜๋ ค๊ณ  ์‹œ๋„ํ•  ๋•Œ, MySQL ์„œ๋ฒ„๊ฐ€ ์•„์ง ์™„์ „ํžˆ ์‹œ์ž‘๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ์ค€๋น„๋˜์ง€ ์•Š์€ ์ƒํƒœ์ผ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ํ™•์ธํ•ด์ฃผ๋Š” ๋กœ์ง์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ํ•œ๋‹ค!

โœ”๏ธ๏ธ depends_on๊ณผ healthcheck์˜ ์ฐจ์ด์ 

  • depends_on : ์„œ๋น„์Šค๊ฐ€ ์‹คํ–‰๋˜์—ˆ๋Š”์ง€๋งŒ ํ™•์ธ, ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜์ง€๋งŒ ์„œ๋น„์Šค๊ฐ€ ์‹ค์ œ๋กœ ์ค€๋น„๋˜์—ˆ๋Š”์ง€๋Š” ๋ณด์žฅํ•˜์ง€ ์•Š์Œ
  • healthcheck : ์„œ๋น„์Šค๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ (์ฆ‰, MySQL์ด ์ค€๋น„๋˜์—ˆ๋Š”์ง€) ํ™•์ธํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ

depends_on๋งŒ ์‚ฌ์šฉํ–ˆ์„ ๊ฒฝ์šฐ, database ์„œ๋น„์Šค๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€! ์ค€๋น„ ์ƒํƒœ์ธ์ง€! ํ™•์ธํ•  ๋ฐฉ๋ฒ•์ด ์—†๋‹ค..


3. ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

thanks to.. ์ตœ์„œ์ง€ (@choiseoji)

  1. ํ™˜๊ฒฝ๋ณ€์ˆ˜ .env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ฃผ์—ˆ๋‹ค!
  2. Healthcheck ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค!

โญ Healthcheck ์˜ ์—ญํ• 

  • ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฒ€์‚ฌํ•˜๋Š” ๊ธฐ๋Šฅ

  • ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ค€๋น„๋˜์—ˆ์„ ๋•Œ๋งŒ ๋‹ค๋ฅธ ์„œ๋น„์Šค๊ฐ€ ์‹œ์ž‘๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์Œ

  • ์˜ˆ์‹œ) database ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ OK!! ๐Ÿ‘‰ application ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ ์‹œ์ž‘!!


4. ์ตœ์ข… ๊ฒฐ๊ณผ

์Šคํฌ๋ฆฐ์ƒท 2024-11-14 140257 ์Šคํฌ๋ฆฐ์ƒท 2024-11-14 140336


๐Ÿ“ข ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… - 2 < Access denied for user 'root'@'172.18.0.1' ์˜ค๋ฅ˜ >

1. ๋ฌธ์ œ์ 

Docker ์—ฐ๊ฒฐ ์ž˜๋๊ณ , 8080๋„ ๋‹ค ์ž˜ ๋–ด์–ด.

๊ทธ๋ž˜์„œ ์ด์ œ API ๋ฆฌํŒฉํ† ๋งํ•˜๊ณ  ์Šคํ”„๋ง๋ถ€ํŠธ run ํ–ˆ๋Š”๋ฐ Access denied for user 'root'@'172.18.0.1' ์—๋Ÿฌ๊ฐ€ ๋œจ๋„ค?

ํ•ด๋‹น ์˜ค๋ฅ˜๋Š” MySQL ์„œ๋ฒ„์—์„œ root ์‚ฌ์šฉ์ž๊ฐ€ IP ์ฃผ์†Œ 172.18.0.1์—์„œ ์ ‘์†์„ ๊ฑฐ๋ถ€๋‹นํ–ˆ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค..


2. ๋ฐœ์ƒ ์›์ธ

MySQL์—์„œ root ์‚ฌ์šฉ์ž๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋กœ์ปฌ์—์„œ๋งŒ ์ ‘์†์„ ํ—ˆ์šฉํ•˜๋„๋ก ์„ค์ •๋œ๋‹ค๊ณ  ํ•œ๋‹ค.

์ฆ‰, root ์‚ฌ์šฉ์ž๊ฐ€ localhost ๋˜๋Š” 127.0.0.1์—์„œ๋งŒ ์ ‘์†ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ œํ•œ๋˜์–ด ์žˆ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.

์ด๋Ÿฌํ•œ ์ƒํ™ฉ์—์„œ Docker ๋„คํŠธ์›Œํฌ ๋‚ด ๋‹ค๋ฅธ ์ปจํ…Œ์ด๋„ˆ (์™ธ๋ถ€ IP) ์—์„œ ์ ‘์†์„ ์‹œ๋„ํ•˜๋Š” ์ƒํ™ฉ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ ‘๊ทผ์„ ์ฐจ๋‹จํ•˜๋Š” ๊ฒƒ์ด์—ˆ๋‹ค!


3. ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

root ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ IP์—์„œ๋„ ์ ‘์†ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ถŒํ•œ์„ ์ˆ˜์ •ํ•ด์•ผ ํ•œ๋‹ค!

  1. MySQL ์ปจํ…Œ์ด๋„ˆ์— ์ ‘์†
docker exec -it <mysql-container-name> mysql -u root -p
  1. root ์‚ฌ์šฉ์ž์— ๋Œ€ํ•ด ์™ธ๋ถ€ ํ˜ธ์ŠคํŠธ์—์„œ์˜ ์ ‘์†์„ ํ—ˆ์šฉํ•˜๋„๋ก ๊ถŒํ•œ ๋ณ€๊ฒฝ
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'your_password' WITH GRANT OPTION;

FLUSH PRIVILEGES;

โ— ์ด๋•Œ, DB_URL ์€ localhost ๋กœ ๋‹ค์‹œ ์ˆ˜์ •ํ•˜๊ธฐ



6. Deploy

๋„์ปค ์ด๋ฏธ์ง€ ๋ฐฐํฌํ•˜๊ธฐ

1. AWS ์ฃผ์š” ์šฉ์–ด ์ •๋ฆฌ

๐Ÿ–ฅ๏ธ EC2 ๋ž€?

EC2 = Elastic Compute Cloud

AWS์—์„œ ์›๊ฒฉ์œผ๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€์ƒ์˜ ์ปดํ“จํ„ฐ ํ•œ ๋Œ€๋ฅผ ๋นŒ๋ฆฌ๋Š” ๊ฒƒ์ด๋‹ค.

EC2๋ฅผ ํ•˜๋‚˜์˜ ์ปดํ“จํ„ฐ๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค!

EC2๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ์ด์œ ?

์„œ๋ฒ„๋ฅผ ๋ฐฐํฌํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ปดํ“จํ„ฐ๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ์ด๋•Œ ๋‚˜์˜ ์ปดํ“จํ„ฐ์—์„œ ์„œ๋ฒ„๋ฅผ ๋ฐฐํฌํ•ด์„œ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋“ค์ด ์ธํ„ฐ๋„ท์„ ํ†ตํ•ด ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‚ด ์ปดํ“จํ„ฐ๋กœ ์„œ๋ฒ„๋ฅผ ๋ฐฐํฌํ•˜๋ฉด 24์‹œ๊ฐ„ ๋™์•ˆ ์ปดํ“จํ„ฐ๋ฅผ ์ผœ๋†”์•ผํ•œ๋‹ค. ๋˜ํ•œ ์ธํ„ฐ๋„ท์„ ํ†ตํ•ด ๋‚ด ์ปดํ“จํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค๋‹ค ๋ณด๋‹ˆ ๋ณด์•ˆ์ ์œผ๋กœ๋„ ์œ„ํ—˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ถˆํŽธํ•จ ๋•Œ๋ฌธ์— ๋‚ด๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์ปดํ“จํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , AWS EC2๋ผ๋Š” ์ปดํ“จํ„ฐ๋ฅผ ๋นŒ๋ ค์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค!

EC2 ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ

EC2 ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๋ฉด, ์„ ํƒํ•œ AMI์— ๋”ฐ๋ผ ์šด์˜์ฒด์ œ, CPU, RAM ๋“ฑ์ด ๋ฏธ๋ฆฌ ๊ตฌ์„ฑ๋œ ์ปดํ“จํ„ฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค!

์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค ๋•Œ, ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค ์œ ํ˜• ๊ฐ€์šด๋ฐ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ฅด๊ณ  ์‚ฌ์ด์ฆˆ ๋“ฑ์„ ๊ณ ๋ฅธ๋‹ค. ์ด๋Ÿฌํ•œ ๊ณผ์ •์„ ํ†ตํ•ด ๋‚ด๊ฐ€ ๋งŒ๋“ค ๊ฐ€์ƒ ์„œ๋ฒ„์˜ ๋ชฉ์ ์— ๋”ฐ๋ผ์„œ ํŠนํ™”๋œ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

AMI

AMI = Amazon Machine Image

๋‚ด๊ฐ€ ์„ ํƒํ•œ ์„œ๋ฒ„ ํŠนํ™” ์˜ต์…˜์„ ๋ชจ์•„๋‘” ๊ฒƒ์œผ๋กœ, ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ์ „์— ๋ฏธ๋ฆฌ ๊ตฌ์„ฑ๋˜๋Š” ์ด๋ฏธ์ง€ ํŒŒ์ผ์ด๋‹ค.

EC2 ์ธ์Šคํ„ด์Šค๋ฅผ ์‹œ์ž‘ํ•  ๋•Œ ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ๋‹ค.

โœ… ์šด์˜ ์ฒด์ œ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„, ์–ธ์–ด ๋Ÿฐํƒ€์ž„, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋“ฑ ์ธ์Šคํ„ด์Šค์—์„œ ์‹คํ–‰๋˜๋Š” ๋ชจ๋“  ์†Œํ”„ํŠธ์›จ์–ด์˜ ์„ค์ •์ด ํฌํ•จ

ํ‚ค ํŽ˜์–ด

EC2 ์ธ์Šคํ„ด์Šค์— ์ ‘์†ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ์•”ํ˜ธํ™”๋œ ํŒŒ์ผ์ด๋‹ค.

๋ฐœ๊ธ‰๋ฐ›์€ ํ”„๋ผ์ด๋น— ํ‚ค๋ฅผ ์ด์šฉํ•˜์—ฌ ์ธ์Šคํ„ด์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ‚ค๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ด€ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค!

EBS ๋ณผ๋ฅจ

EBS = Elastic Block Storage

EBS๋ž€, ํด๋ผ์šฐ๋“œ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ€์ƒ ํ•˜๋“œ๋””์Šคํฌ์ด๋‹ค.

EC2 ์ธ์Šคํ„ด์Šค๊ฐ€ ์—ฐ์‚ฐ(CPU, ๋ฉ”๋ชจ๋ฆฌ)์— ๊ด€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ๋‹ค๊ณ  ํ•˜๋ฉด, ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ์—ญํ• ์€ ๋ฐ”๋กœ EBS๊ฐ€ ํ•œ๋‹ค๊ณ  ๋ณด๋ฉด ๋œ๋‹ค!

EBS ๋ณผ๋ฅจ์ด๋ž€, EBS๋กœ ์ƒ์„ฑํ•œ ๋””์Šคํฌ ํ•˜๋‚˜ํ•˜๋‚˜๋ฅผ ๋œปํ•˜๋Š” ์ €์žฅ ๋‹จ์œ„๋ฅผ ๋งํ•œ๋‹ค.

์‰ฝ๊ฒŒ ๋งํ•ด, ์œˆ๋„์šฐ์˜ C๋“œ๋ผ์ด๋ธŒ, D๋“œ๋ผ์ด๋ธŒ๋Š” ๊ฐ๊ฐ์˜ ๋””์Šคํฌ์ด๋ฉฐ EBS ๋ณผ๋ฅจ์ด๋‹ค!

๋ณด์•ˆ๊ทธ๋ฃน

EC2 ์ธ์Šคํ„ด์Šค์— ํ—ˆ์šฉ๋˜๋Š” ์ธ๋ฐ”์šด๋“œ, ์•„์›ƒ๋ฐ”์šด๋“œ ํŠธ๋ž˜ํ”ฝ์„ ์ œ์–ดํ•˜๋Š”๊ฐ€์ƒ ๋ฐฉํ™”๋ฒฝ์ด๋‹ค.

์ฆ‰, ์—ฐ๊ฒฐ๋œ ๋ฆฌ์†Œ์Šค์— ๋„๋‹ฌํ•˜๊ณ  ๋‚˜๊ฐˆ ์ˆ˜ ์žˆ๋Š” ํŠธ๋ž˜ํ”ฝ์„ ์ œ์–ดํ•œ๋‹ค.

โญ ์ธ๋ฐ”์šด๋“œ

  • ์™ธ๋ถ€์—์„œ ์ธ์Šคํ„ด์Šค์— ์ ‘๊ทผํ•˜๋Š” ํŠธ๋ž˜ํ”ฝ์— ๋Œ€ํ•œ ํ—ˆ์šฉ ๋ฒ”์œ„ ์ œ์–ด

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ž์‹ ์˜ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ์— ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๋Š” ๊ทœ์น™

  • ๊ธฐ๋ณธ์ ์œผ๋กœ ์ธ๋ฐ”์šด๋“œ ๊ทœ์น™์€ ๋ชจ๋“  ํฌํŠธ๋ฅผ ๋‹ซ๋Š” ๊ฒƒ์„ ์ „์ œ

โญ ์•„์›ƒ๋ฐ”์šด๋“œ

  • ์„œ๋ฒ„์—์„œ ์™ธ๋ถ€๋กœ ๋‚˜๊ฐ€๋Š” ํŠธ๋ž˜ํ”ฝ์— ๋Œ€ํ•œ ํ—ˆ์šฉ ๋ฒ”์œ„ ์ œ์–ด

  • ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ์•„์›ƒ๋ฐ”์šด๋“œ ํŠธ๋ž˜ํ”ฝ์„ ํ—ˆ์šฉ

์Šค์™‘ ๋ฉ”๋ชจ๋ฆฌ

์‹ค์ œ ๋ฉ”๋ชจ๋ฆฌ RAM์ด ๊ฐ€๋“ ์ฐผ์ง€๋งŒ ๋” ๋งŽ์€ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ํ•„์š”ํ• ๋•Œ ๋””์Šคํฌ ๊ณต๊ฐ„์„ ์ด์šฉํ•˜์—ฌ ๋ถ€์กฑํ•œ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ๋Š” ๊ณต๊ฐ„์„ ์˜๋ฏธํ•œ๋‹ค.

EC2 ํ”„๋ฆฌํ‹ฐ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ RAM์ด 1GB์ด๊ธฐ ๋•Œ๋ฌธ์— ๋นŒ๋“œ๋‚˜ ์‹คํ–‰์„ ์ง„ํ–‰ํ•˜๋‹ค ์ปดํ“จํ„ฐ๊ฐ€ ๋ฉˆ์ถœ ์ˆ˜ ์žˆ๋‹ค!

์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์Šค์™‘ ๋ฉ”๋ชจ๋ฆฌ ์„ค์ •์ด๋ผ๊ณ  ํ•œ๋‹ค.

๐Ÿ–ฅ๏ธ IP ์ฃผ์†Œ ์ข…๋ฅ˜

์ฒ˜์Œ EC2 ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค๋ฉด ํผ๋ธ”๋ฆญ IPv4 ์ฃผ์†Œ์™€ ํ”„๋ผ์ด๋น— IPv4 ์ฃผ์†Œ๊ฐ€ ํ• ๋‹น๋œ๋‹ค.

ํผ๋ธ”๋ฆญ IP

  • ์ธํ„ฐ๋„ท ์ƒ์—์„œ ๊ฐœ๊ฐœ์ธ์˜ ๋กœ์ปฌ ๋„คํŠธ์›Œํฌ๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด ISP์—์„œ ์ œ๊ณตํ•˜๋Š” IP ์ฃผ์†Œ

  • ์™ธ๋ถ€์— ๊ณต๊ฐœ๊ฐ€ ๋˜์–ด์žˆ์–ด์„œ ๋‹ค๋ฅธ ์ธํ„ฐ๋„ท ์‚ฌ์šฉ์ž๋“ค์ด ๋‚˜์—๊ฒŒ ์ ‘์† ํ•  ์ˆ˜ ์žˆ๋‹ค.

SSH ์ ‘์† ์‹œ ์ธ์Šคํ„ด์Šค์˜ ํผ๋ธ”๋ฆญ ์ฃผ์†Œ๋ฅผ ํ†ตํ•ด ์ธ์Šคํ„ด์Šค ๋‚ด๋ถ€๋กœ ์ ‘์†ํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ”„๋ผ์ด๋น— IP

  • ์™ธ๋ถ€์—์„œ๋Š” ์ ‘์†ํ•  ์ˆ˜ ์—†๋Š” ๋„คํŠธ์›Œํฌ ๋ง

์šฐ๋ฆฌ๋Š” ์ด ์ฃผ์†Œ์— ์ง์ ‘์ ์œผ๋กœ๋Š” ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•œ๋‹ค. ์˜ค์ง ์™ธ๋ถ€๋กœ ์—ด๋ ค์žˆ๋Š” ํผ๋ธ”๋ฆญ IP๋ฅผ ํ†ตํ•ด์„œ๋งŒ ์ธ์Šคํ„ด์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค!

ํƒ„๋ ฅ์  IP

ํผ๋ธ”๋ฆญ IP๋งŒ์œผ๋กœ๋„ EC2 ์ธ์Šคํ„ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ๋Š” ๋ณ„ ๋ฌธ์ œ๊ฐ€ ์—†์–ด๋ณด์ด๋Š”๋ฐ ํƒ„๋ ฅ์  ์ฃผ์†Œ๋Š” ์™œ ์žˆ๋Š”๊ฒƒ์ผ๊นŒ?

AWS๋Š” ์ธ์Šคํ„ด์Šค๊ฐ€ ์ฒ˜์Œ ์ƒ์„ฑ๋˜๊ฑฐ๋‚˜, ์ค‘์ง€ ํ›„ ๋‹ค์‹œ ์‹œ์ž‘ํ•  ๋•Œ๋งˆ๋‹ค ํผ๋ธ”๋ฆญ IP๋ฅผ ์žฌํ• ๋‹นํ•œ๋‹ค. IP ์ฃผ์†Œ๊ฐ€ ๋งค๋ฒˆ ๋ฐ”๋€Œ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค!

์„œ๋น„์Šค ์šด์˜์˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ์ค‘์ง€ ํ›„ ์žฌ์‹œ์ž‘๋งˆ๋‹ค ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ๊ณ ์ •๋œ IP ์ฃผ์†Œ๊ฐ€ ํ•„์ˆ˜์ ์ด๋‹ค!

๐Ÿ–ฅ๏ธ RDS ๋ž€?

RDS = Relational Database Service

๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” AWS์˜ ์„œ๋น„์Šค์ด๋‹ค.

RDS๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, AWS์—์„œ ๋ชจ๋“  ๊ฒƒ์„ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ถ€๋ถ„์— ๋Œ€ํ•ด ์‹ ๊ฒฝ์„ ์“ฐ์ง€ ์•Š๊ณ  ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค!

VPC ๋ณด์•ˆ๊ทธ๋ฃน

VPC = Virtual Private Cloud

image ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๊ฐ™์€ ํด๋ผ์šฐ๋“œ ์ƒ์— ์žˆ์ง€๋งŒ, ๋ณด์•ˆ์ƒ์˜ ๋ชฉ์ ์„ ์œ„ํ•ด ๋…ผ๋ฆฌ์ ์œผ๋กœ ๋‹ค๋ฅธ ํด๋ผ์šฐ๋“œ์ธ ๊ฒƒ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋„๋ก ๋งŒ๋“  ๊ฐ€์ƒ ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์ด๋‹ค.

VPC๋ณ„๋กœ ๋‹ค๋ฅธ ๋„คํŠธ์›Œํฌ ์„ค์ •์„ ํ•  ์ˆ˜ ์žˆ๊ณ , ๋…๋ฆฝ๋œ ๋„คํŠธ์›Œํฌ์ฒ˜๋Ÿผ ์ž‘๋™ํ•œ๋‹ค.

2. ์ˆ˜๋™ ๋ฐฐํฌํ•˜๊ธฐ

์ธํ…”๋ฆฌ์ œ์ด (๋‚ด ์Šคํ”„๋ง ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜)

// ํ…Œ์ŠคํŠธ ์ƒ๋žตํ•˜๊ณ  ์Šคํ”„๋ง ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋นŒ๋“œ
./gradlew clean build -x test

// ๋„์ปค ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋“œํ•˜๊ณ  ์ƒ์„ฑํ•˜๋Š” ๋ช…๋ น์–ด
docker build --platform linux/amd64 -t [๋„์ปค์•„์ด๋””]/[๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช…] 

// ๋„์ปค ํ—ˆ๋ธŒ์— ์ด๋ฏธ์ง€ ์˜ฌ๋ฆฌ๊ธฐ
docker push [๋„์ปค์•„์ด๋””]/[๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช…]
  • docker build ๋Š” ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ์— ์žˆ๋Š” Dockerfile ๊ธฐ๋ฐ˜์œผ๋กœ ์ด๋ฏธ์ง€ ์ƒ์„ฑ

โœ… ๋‚ด ์Šคํ”„๋ง๋ถ€ํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ๊ณผ ํŒŒ์ผ(JAR ํŒŒ์ผ)์ด ๋“ค์–ด์žˆ๋Š” ์ด๋ฏธ์ง€

SSH ์ ‘์† (์ฝ˜์†”์ฐฝ)

// ํŒจํ‚ค์ง€ ์—…๋ฐ์ดํŠธ
sudo apt update

// ๋„์ปค ์„ค์น˜
sudo apt install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
sudo apt update
sudo apt install docker-ce
docker --version

// Docker Hub์—์„œ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ
sudo docker pull [๋„์ปค์•„์ด๋””]/[๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช…]

// ์ด๋ฏธ์ง€ ๊ธฐ๋ฐ˜์œผ๋กœ ๋„์ปค ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰
sudo docker run -e .env -d -p 80:8080 [๋„์ปค์•„์ด๋””]/[๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช…]
  • -p 80:8080 : ํ˜ธ์ŠคํŠธ์˜ ํฌํŠธ 80์„ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์˜ ํฌํŠธ 8080์œผ๋กœ ๋งคํ•‘

.env

DB_URL=jdbc:mysql://{RDS ์—”๋“œํฌ์ธํŠธ ์ฃผ์†Œ}:3306/{RDS ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ด๋ฆ„}
DB_USERNAME=
DB_PASSWORD=

์ตœ์ข… ๊ฒฐ๊ณผ

์Šคํฌ๋ฆฐ์ƒท 2024-11-22 224604

โœ…ํผ๋ธ”๋ฆญ ์ฃผ์†Œ๋ฅผ ํ†ตํ•ด ๋“ค์–ด๊ฐ€๋ณด๋ฉด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋œจ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค!


๋ฐฐํฌํ™˜๊ฒฝ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ

์Šคํฌ๋ฆฐ์ƒท 2024-11-23 010424

ํฌ์ŠคํŠธ๋งจ์„ ํ†ตํ•ด API ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด๋ณด์•˜๋Š”๋ฐ 401 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€๋‹ค.

signup API์˜ ๊ฒฝ์šฐ, ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ permitAll ์„ค์ •์„ ํ•ด์ฃผ์—ˆ๋Š”๋ฐ๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์™œ 401 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ• ๊นŒ? ์‘๋‹ต ํ˜•ํƒœ๋„ ์ด์ƒํ•˜๋‹ค..

์ด๊ฑฐ ์™œ ์ด๋Ÿฌ๋Š” ๊ฑธ๊นŒ์š”์˜ค..๐Ÿฅฒ

ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค..

๋„ํ˜„๋‹˜ ) validation ํ•˜๊ธฐ ์ „์— ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ๋Š๋‚Œ์ด๋‹ค.. ํ•„ํ„ฐ์ชฝ์— ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š”๊ฑฐ๋ฉด ํ—ค๋”์— ํ† ํฐ์„ ๋„ฃ์—ˆ๋‹ค๋˜๊ฐ€.. ํ—ค๋”๋ฅผ ํ™•์ธํ•ด๋ด๋ผ

๋„ค ๋งž์Šต๋‹ˆ๋‹ค. ์ €๋Š” ์š”์ฒญ ํ—ค๋”์— ๊ธฐ์กด์— ํ…Œ์ŠคํŠธํ–ˆ๋˜ Authorization ํ•„๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ๋„ฃ๊ณ  ํšŒ์›๊ฐ€์ž… ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค..

์ด๊ฑธ ๋นผ๋‹ˆ๊นŒ ๋„ˆ๋ฌด ์ž˜๋˜๋”๋ผ๊ตฌ์š”..

๋ฐฐํฌ๊ฐ€ ์ž˜๋˜์—ˆ๋‹ค๋‹ˆ.. ๋‹คํ–‰์ž…๋‹ˆ๋‹ค ํ‘๐Ÿฅฒ

์Šคํฌ๋ฆฐ์ƒท 2024-11-24 014331

About

CEOS 20th BE study - instagram clone coding

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java 99.9%
  • Dockerfile 0.1%