CEOS 20th BE study - instagram clone coding
- ๊ฒ์๊ธ ์กฐํ
- ๊ฒ์๊ธ์ ์ฌ์ง๊ณผ ํจ๊ป ๊ธ ์์ฑํ๊ธฐ
- ๊ฒ์๊ธ์ ๋๊ธ ๋ฐ ๋๋๊ธ ๊ธฐ๋ฅ
- ๊ฒ์๊ธ์ ์ข์์ ๊ธฐ๋ฅ
- ๊ฒ์๊ธ, ๋๊ธ, ์ข์์ ์ญ์ ๊ธฐ๋ฅ
- ์ ์ ๊ฐ 1:1 DM ๊ธฐ๋ฅ
์ด 6๊ฐ์ง ๊ธฐ๋ฅ์ ๋ง์ถ์ด ์ธ์คํ๊ทธ๋จ ๋ฐ์ดํฐ ๋ชจ๋ธ๋ง์ ์งํํด๋ณด์๋ค.
๋๋ฉ์ธ์ ์ด 5๊ฐ (/member
, /post
, /comment
, /hashtag
, /chat
) ๋ก ๊ตฌ์ฑํ์๋ค.
- ๊ฐ์ ์ ์ํด์๋ ํด๋ํฐ ๋๋ ์ด๋ฉ์ผ ์ธ์ฆ์ ๊ฑฐ์ณ์ผํ๋ค.
- ์ธ์ฆ ํ์๋ ๋น๋ฐ๋ฒํธ, ์ด๋ฆ, ์ฌ์ฉ์์ด๋ฆ(๋๋ค์)์ ๊ธฐ์ ํ๋ค.
- ๋ง์ดํ์ด์ง์๋ ํ๋กํ์ด๋ฏธ์ง, ์ด๋ฆ, ์ฌ์ฉ์์ด๋ฆ, ์๊ฐ, ๋งํฌ, ์์ฑํ ๊ฒ์๊ธ ๋ฑ์ด ํฌํจ๋๋ค.
- ๋ชจ๋ ํ์์ ์ํ๋
active
,inactive
์ค ํ๋์ด๋ค. - ํ์ ํํด ์,
inactive
์ํ๋ก ๋๊ณ ์ผ์ ๊ธฐ๊ฐ๋์ ๋นํ์ฑ์ธ ๊ฒฝ์ฐ ์๋ ํํด ์ฒ๋ฆฌ๊ฐ ์ด๋ฃจ์ด์ง๋ค.
- ๊ฒ์๊ธ์๋ ๋ด์ฉ, ์์น, ์ด๋ฏธ์ง(์ต๋ 10์ฅ), ์์ ๋ฑ์ ๋ฃ์ ์ ์๋ค.
- ๊ฒ์๊ธ์ ํ๋กํ ์ฌ์ง, ์ฌ์ฉ์ ์ด๋ฆ, ์ด๋ฏธ์ง, ๋๊ธ ๊ฐ์, ์์น, ์์ , ํด์ํ๊ทธ ๋ฑ์ ํฌํจํ๋ค.
- ๋๊ธ์๋ ๋ด์ฉ, ์ข์์ ๊ฐ์ ๋ฑ์ด ํฌํจ๋๋ค.
- ๋๊ธ์๋ ๋ถ๋ชจ ๋๊ธ์ ๊ธฐ์ค์ผ๋ก ๋๋๊ธ์ด ๋ฌ๋ฆด ์ ์๋ค.
- ๋๋๊ธ์ ์์ ์ด ๋ฌ๋ฆฐ ๋ถ๋ชจ ๋๊ธ์ ์ฐธ์กฐ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์๋ค.
- ๋ชจ๋ ๋๊ธ์๋ ์ข์์๋ฅผ ๋๋ฅผ ์ ์๋ค.
- ์ฌ์ฉ์์ ๋ ๋ค๋ฅธ ์ฌ์ฉ์ ๊ฐ์ ๋์ ์ ์ฃผ๊ณ ๋ฐ๋ ๊ณต๊ฐ์ด๋ค.
- ์ฌ์ฉ์ ๊ฐ ์ผ๋์ผ๋ก ์งํ๋๋ฉฐ, ์ค์๊ฐ์ผ๋ก ์ด๋ฃจ์ด์ง๋ค.
-
ํ์ - ๊ฒ์๊ธ, ํ์ - ๋ฉ์ธ์ง, ํ์ - ์ฑํ ๋ฐฉ, ํ์ - ๋๊ธ์ข์์, ํ์ - ๊ฒ์๊ธ์ข์์, ํ์ - ๋๊ธ (
1:N
์ผ๋๋ค๊ด๊ณ): ํ ๋ช ์ ํ์์ ์ฌ๋ฌ ๊ฐ์ ๊ฒ์๊ธ / ๋ฉ์ธ์ง / ์ฑํ ๋ฐฉ / ๋๊ธ์ข์์ / ๊ฒ์๊ธ์ข์์ / ๋๊ธ์ ๊ฐ์ง์ง๋ง, ์ด๋ค์ ํ ๋ช ์ ํ์์๋ง ์ํ๋ค.
-
์ฑํ ๋ฐฉ - ๋ฉ์ธ์ง (
1:N
์ผ๋๋ค๊ด๊ณ): ํ๋์ ์ฑํ ๋ฐฉ์ ์ฌ๋ฌ ๊ฐ์ ๋ฉ์ธ์ง๋ฅผ ํฌํจํ ์ ์์ง๋ง, ํ๋์ ๋ฉ์ธ์ง๋ ํ๋์ ์ฑํ ๋ฐฉ์๋ง ์ํ๋ค.
-
๊ฒ์๊ธ - ๋๊ธ, ๊ฒ์๊ธ - ๊ฒ์๊ธ์ข์์, ๊ฒ์๊ธ - ์ด๋ฏธ์ง (
1:N
์ผ๋๋ค๊ด๊ณ): ํ๋์ ๊ฒ์๊ธ์ ์ฌ๋ฌ ๊ฐ์ ๋๊ธ / ๊ฒ์๊ธ์ข์์ / ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ง์ง๋ง, ์ด๋ค์ ํ๋์ ๊ฒ์๊ธ์๋ง ์ํ๋ค.
-
๊ฒ์๊ธ - ๊ฒ์๊ธํด์ํ๊ทธ, ํด์ํ๊ทธ - ๊ฒ์๊ธํด์ํ๊ทธ (
1:N
์ผ๋๋ค๊ด๊ณ)-
๊ฒ์๊ธ - ํด์ํ๊ทธ
๊ฐ ์ฐ๊ฒฐ์ ์ํด ์ค๊ฐ ์ํฐํฐ์ธ ๊ฒ์๊ธํด์ํ๊ทธ๋ฅผ ํ์ฉํ์๋ค. -
ํ๋์ ๊ฒ์๊ธ์ ์ฌ๋ฌ ๊ฒ์๊ธํด์ํ๊ทธ๋ฅผ ๊ฐ์ง ์ ์๊ณ , ๋ง์ฐฌ๊ฐ์ง๋ก ํด์ํ๊ทธ๋ ์ฌ๋ฌ ๊ฒ์๊ธํด์ํ๊ทธ๋ฅผ ๊ฐ์ง ์ ์๋ค. ์ด๋ ๊ฒ์๊ธํด์ํ๊ทธ๋ ํ๋์ ๊ฒ์๊ธ๊ณผ ํด์ํ๊ทธ์๋ง ์ข ์๋๋ค.
-
-
๋๊ธ - ๋๊ธ์ข์์ (
1:N
์ผ๋๋ค๊ด๊ณ): ํ๋์ ๋๊ธ์ ์ฌ๋ฌ ๊ฐ์ ๋๊ธ์ข์์๋ฅผ ๊ฐ์ง์ง๋ง, ํ๋์ ๋๊ธ์ข์์๋ ํ๋์ ๋๊ธ์๋ง ์ํ๋ค.
๊ณตํต ํ๋๋ฅผ ์ ์ํ์ฌ ๋ค๋ฅธ ์ํฐํฐ๋ค์ด ๊ณตํต์ ์ผ๋ก ์ฌ์ฉํ ์ ์๊ฒ ๋ถ๋ฆฌํ ์ถ์ ํด๋์ค์ด๋ค. ๋ค๋ฅธ ์ํฐํฐ๋ค์ ํด๋น ์ถ์ ํด๋์ค๋ฅผ ์์๋ฐ์ ๊ณตํต ํ๋๋ฅผ ํธ๋ฆฌํ๊ฒ ๊ฐ์ ธ๋ค ์ฌ์ฉํ ์ ์๋ค.
@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;
}
์ด ํด๋์ค๋ฅผ ์์๋ฐ๋ ๋ค๋ฅธ ์ํฐํฐ๋ค์ด ๊ณตํต๋ ํ๋๋ฅผ ๊ฐ์ง๋๋ก ํจ
JPA Auditing
๊ธฐ๋ฅ ํ์ฑํAuditingEntityListener
๋ ์ํฐํฐ ์์ฑ ๋ฐ ์์ ์์ ์ ์๋์ผ๋ก ๊ธฐ๋กํจ@CreatedDate
์@LastModifiedDate
๊ฐ ์ด ๋ฆฌ์ค๋๋ฅผ ํตํด ์๋์ผ๋ก ๊ด๋ฆฌ
JPA Auditing
์ ์ด๋ ธํ ์ด์ - ์ํฐํฐ๊ฐ ์์ฑ๋ ๋ ์๋์ผ๋ก ํ์ฌ ๋ ์ง์ ์๊ฐ์ผ๋ก ํ๋ ์ค์
JPA Auditing
์ ์ด๋ ธํ ์ด์ - ์ํฐํฐ๊ฐ ์์ ๋ ๋ ์๋์ผ๋ก ํ์ฌ ๋ ์ง์ ์๊ฐ์ผ๋ก ํ๋๋ฅผ ์ ๋ฐ์ดํธํจ
Springboot Application
์์ฒด๋ JpaAuditing
์ฌ์ฉ์ด ๊ฐ๋ฅํ๋๋ก ๋ณ๊ฒฝํด์ฃผ์ด์ผ ํจ
@SpringBootApplication
@EnableJpaAuditing
public class InstagramCloneApplication {
public static void main(String[] args) {
SpringApplication.run(InstagramCloneApplication.class, args);
}
}
Builder
๋ฅผ ์ ์ฉํ๋ ๋ฐฉ์์ด ํฌ๊ฒ 2๊ฐ์ง๊ฐ ์์์ ์๊ฒ ๋์๋ค.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Member extends BaseEntity {
- ํด๋์ค์ ๋ชจ๋ ์์ฑ์์ ๋ํด ๋น๋ ํจํด ์ ์ฉ ๊ฐ๋ฅ
- ํด๋์ค ์ ์ฒด์์ ๋์ผํ ๋ฐฉ์์ผ๋ก ๊ฐ์ฒด๋ฅผ ์์ฑํ ์ ์์ด ์ฝ๋์ ์ผ๊ด์ฑ์ด ์ ์ง๋จ
- ์์ฑ์์ ๋ฐ๋ผ ๋น๋๋ฅผ ์ ์ฉํ๋ ๋ฐ์ ์์ด ํผ๋์ด ๋ฐ์ํ ๊ฐ๋ฅ์ฑ ์กด์ฌ
@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(length = 50, nullable = false)
private String nickname;
@Column(name = "created_at", nullable = false, columnDefinition = "timestamp")
private LocalDateTime createdAt;
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด ์ด๋ฆ ์ง์ ์ง์
- ๊ธฐ๋ณธ๊ฐ์ ํ๋๋ช
- ์ด์ ์ต๋ ๊ธธ์ด ์ค์
length = 50
์varchar(50)
์ด๋ผ๋ ์๋ฏธ
- ์ด์ด
NULL
๊ฐ์ ํ์ฉํ๋์ง์ ๋ํ ์ฌ๋ถ๋ฅผ ์ง์ @Column(nullable = false)
๋, ํด๋น ํ๋๋NULL
๊ฐ์ ํ์ฉํ์ง ์๋๋ค๋ ์๋ฏธ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด์ SQL ๋ฐ์ดํฐ ์ ํ๊ณผ ์์ฑ์ ์ง์ ์ ์ํ ๋ ์ฐ์
text
์timestamp
์ ํ์ ๊ฒฝ์ฐ, ๋ฐ๋ก ๋ช ์ํด์ฃผ์ด์ผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์๋จ- ์)
@Column(columnDefinition = "text")
@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
๋ก ์ ์ธ๋ ๋ฉ์๋๋ ๋ฐ๋์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(), "์ญ์ ๋ ๊ฒ์๋ฌผ์ด๋ฏ๋ก ์กด์ฌํ๋ฉด ์๋ฉ๋๋ค!");
}
-
assertEquals (
์์๊ฐ
,์ค์ ๊ฐ
,์คํจ ์ ์ถ๋ ฅ๋๋ ๋ฉ์์ง
) : ์์ ๊ฐ๊ณผ ์ค์ ๊ฐ์ ๋น๊ตํ์ฌ ์ผ์นํ๋์ง ํ์ธ -
assertTrue (
์กฐ๊ฑด
,์คํจ ์ ์ถ๋ ฅ๋๋ ๋ฉ์์ง
) : ์ฃผ์ด์ง ์กฐ๊ฑด์ด ์ฐธ์ธ์ง ํ์ธ -
assertFalse(
์กฐ๊ฑด
,์คํจ ์ ์ถ๋ ฅ๋๋ ๋ฉ์์ง
) : ์ฃผ์ด์ง ์กฐ๊ฑด์ด ๊ฑฐ์ง์ธ์ง ํ์ธ
- ๋ฌผ๋ฆฌ์ ์ญ์
- ๋ฐ์ดํฐ๋ฅผ ์ค์ ๋ก ์ญ์ ํ๋ ๋ฐฉ๋ฒ
- ์ญ์ ๋ ๋ฐ์ดํฐ๋ ์์คํ ์์ ์์ ํ ์ ๊ฑฐ๋์ด ๋ณต๊ตฌ ๋ถ๊ฐ
- ๋ ผ๋ฆฌ์ ์ญ์
- ๋ฐ์ดํฐ๋ฅผ ์ค์ ๋ก ์ญ์ ํ์ง ์๊ณ , ์ญ์ ๋ ๊ฒ์ฒ๋ผ ๋ณด์ด๊ฒ ํจ
- ๋ฐ์ดํฐ๋ ์์คํ ์์ ๋ ์ด์ ์ฌ์ฉ๋์ง ์์ง๋ง, ํ์ํ ๊ฒฝ์ฐ ๋๋๋ฆฌ๊ธฐ ๊ฐ๋ฅ
- ๋ฐ์ดํฐ ๋ณด์กด์ ์ํด ์ ์ฉํ๋ฉฐ, ์ค์๋ก ์ญ์ ๋ ๋ฐ์ดํฐ ๋ณต๊ตฌ ๊ฐ๋ฅ
- ๊ทธ๋ฌ๋! ์ญ์ ๋ ๋ฐ์ดํฐ ์ ์งํ๋ ค๋ฉด ์ถ๊ฐ์ ์ธ ์ ์ฅ ๊ณต๊ฐ์ด ํ์ํ๊ธฐ ๋๋ฌธ์ ์ ์คํ๊ฒ ์ฌ์ฉํ ๊ฒ,,
- ํํด๋ฅผ ํ๋ค๊ฐ ์ผ์ฃผ์ผ ๋ด๋ก ๋ค์ ๋์์ฌ ๊ฐ๋ฅ์ฑ ์กด์ฌ
- ํ์์ ๋ํ ๋ถ์, ํต๊ณ์ ํ์์ฑ
- ํํด๊ฐ ์ด๋ฃจ์ด์ง ๊ฒฝ์ฐ์๋, ๋ค๋ฅธ ์๋น์ค๋ ๋น์ฆ๋์ค ๋ก์ง์์ ์ด ํ์๊ณผ ๊ด๋ จ๋ ๋ฐ์ดํฐ์ ์ ๊ทผ ๊ฐ๋ฅํด์ผ ํจ
์ด๋ฌํ ์ด์ ๋ค๋ก ์ธํด Member
์ ๋ํ ๋ฐ์ดํฐ ์ญ์ ์ ๊ฒฝ์ฐ๋ 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 {
- ์ญ์ ์์ฒญ์ด ๋ค์ด์์ ๋ ์คํ๋๋ SQL ๊ตฌ๋ฌธ ์ ์ =>
UPDATE
์คํ - ๋ฐ์ดํฐ๋ ์ญ์ ๋์ง ์๊ณ , ์ญ์ ๋ ์ํ ๋ก ํ์ ๋จ
deleted_at
์ดNULL
์ธ, ์ฆ ์ญ์ ๋์ง ์์ ๋ ์ฝ๋๋ง ์กฐํ๋๊ฒ ํจ- ์ญ์ ๋ ๋ ์ฝ๋๋ ์กฐํ๋์ง ์์
@Column(name = "comment_count")
private int commentCount = 0;
@Column(name = "comment_count")
@Builder.Default
private int commentCount = 0;
๋ด๊ฐ ์๋ ํ๋ ๋ฐฉ์์ผ๋ก ํ ๊ฒฝ์ฐ,
Builder
ํจํด์์ ๊ธฐ๋ณธ๊ฐ์ ์ง์ ์ค์ ํ์ง ์๋๋ค๋ฉด 0์ด ์๋ ๊ฐ์ผ๋ก ์ค์ ๋ ์ฌ์ง๊ฐ ์๋ค๊ณ ํ๋ค
ํ๋ก๊ทธ๋จ ์ ์ฒด์ Builder
ํจํด์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ด๊ฐ ์ด๊ธฐํํ ๊ธฐ๋ณธ๊ฐ์ด ํ์คํ ๋ณด์ฅ๋๋ @Builder.Default
์ ์ด์ฉํ๋ ๊ฒ์ด ๋ ๋์ ๋ฐฉ์!!
์ถ๊ฐ์ ์ผ๋ก, ์ํฐํฐ์ new ArrayList<>()
์๋ ๋ง์ฐฌ๊ฐ์ง๋ก @Builder.Default
๋ฅผ ์ ์ฉํด์ฃผ์๋ค.
@Builder.Default
๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฉด null ์ํ์ ๋ฆฌ์คํธ์ ์ ๊ทผํ๊ฒ ๋์ด ์๋ฌ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ด๋ค!!!
ํ๋๋ ์ด๊ธฐํ๋์ง ์๊ณ null ์ํ๋ก ๋จ๊ฒ๋๋ค. ์ด ์ํ์์ ์ ๊ทผ์ ์๋ํ๋ค๋ฉด, NullPointerException
์ด ํฐ์ง๊ฒ ๋๋ค..
๋ฐ๋ผ์, @OneToMany
๊ด๊ณ์์ List<Post> posts = new ArrayList<>()
์ ๊ฐ์ ์ปฌ๋ ์
ํ์
ํ๋๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํด๋น ํ๋์ ๋น ์ปฌ๋ ์
์ ํ ๋นํด์ค์ผํ๋ค.
์ฝ๋ ๋ด (์ฃผ๋ก Service
์ฝ๋) ์์ ๋ฐ์ํ๋ ์ค๋ฅ๋ฅผ ์ฒด๊ณ์ ์ด๊ณ ์ผ๊ด๋๊ฒ ๊ด๋ฆฌํ๊ธฐ ์ํด์ ์์ธ์ฒ๋ฆฌ๊ตฌ์กฐ ๋ฅผ ๋์
ํ๋ค๊ณ ํ๋ค.
์์ธ์ฒ๋ฆฌ๊ตฌ์กฐ๋ฅผ ์ํด์๋ ์ด 4๊ฐ์ง ํ์ผ์ด ์๊ตฌ๋๋ค.
@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
์ ์ฐ๋๋จ
@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;
}
- ๋ค์ํ ์์ธ ์ํฉ์ ๋ํ ์ฝ๋์ ๋ฉ์์ง๋ฅผ ๊ด๋ฆฌ
- ์ฝ๋์ ๋ฉ์์ง๋ฅผ ์ค์์์ ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ์ ์ง๋ณด์๊ฐ ์ฉ์ด
- ์ฌ์ ์ ์ ์๋ ์์ธ ์ฝ๋์ ๋ฉ์์ง๋ฅผ ์ ๊ณตํ์ฌ ์์ธ ์ฒ๋ฆฌ๋ฅผ ์ผ๊ด์ฑ ์๊ฒ ์ ์งํ๊ฒ ํจ
@Getter
@RequiredArgsConstructor
public class ExceptionResponse {
private final int code;
private final String message;
}
- ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐํํ ์์ธ ์๋ต ๊ฐ์ฒด
- ์์ธ ์ฝ๋์ ๋ฉ์์ง๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌ
@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
๋ฉ์๋์์ ์ ์ํ ์๋ต ํ์๋๋ก ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐํ๋จ
// 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์ ํด๋นํ๋ ๋ฉค๋ฒ๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค."
}
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();
}
์ผ๋จ, return
๊ฐ์ ๋น๋๊ฐ ๋ค์ด๊ฐ์๊ธฐ ๋๋ฌธ์ ๋น์ฆ๋์ค ๋ก์ง์ ๊ฐ๋
์ฑ์ด ๋จ์ด์ง๊ณ ๋ณต์กํด๋ณด์ธ๋ค.
DTO
์ ๊ฒฝ์ฐ, ์ฌ์ฉํ๋ ์ธ์คํด์ค๋ค์ด ๊ฒน์นจ์๋ ๋ถ๊ตฌํ๊ณ ๋ชจ๋ DTO
ํ์ผ์ ๊ธฐ๋ฅ์ ๋ฐ๋ผ ํ๋ํ๋์ฉ ๋ค ๋ง๋ค์ด๋์ ๊ฒ์ ๋ณผ ์ ์๋ค.
๐ก Service
์ฝ๋์ builder๋ฅผ ์์ ๊ณ Validation - Business Logic - Response
์ ์ง์คํด์ ์ฝ๋์ ๊ฐ๋
์ฑ์ ๋์ด์!
๐ก ์ฐ์ด๋ ์ธ์คํด์ค๊ฐ ๋น์ทํ DTO
๋ค์ ํ๋์ ํ์ผ ์์ ๋ฃ๊ณ , DTO
์์ builder๋ฅผ ๋ฃ์ด๋ณด์!
์ด๋ฌํ ์ด์ ๋ค๋ก ๋ ๋์ DTO
์์ฑ๋ฐฉ์์ ๋ํด ์์๋ณด๋ค๊ฐ record
๋ฅผ ํ์ฉํ DTO
์ ๋ํด ์๊ฒ ๋์๋ค!
record
๋ ๋ณธ๋ ๋ฐ์ดํฐ ์ ๋ฌ์ ์ํ ๋จ์ํ ๊ตฌ์กฐ์ฒด ์ญํ ์ ํ๊ธฐ ์ํด ์ค๊ณ๋ ๊ฒ- ๋ถ๋ณ ๊ฐ์ฒด๋ฅผ ๋ค๋ฃจ๊ธฐ ์ํด ๋ง๋ค์ด์ก๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ ๋จ์ํ ์ ๋ฌํ๋ DTO์ ์ ํฉ
- ์๋์ผ๋ก ์์ฑ์, getter,
equals
,hashCode
,toString
๋ฉ์๋๋ฅผ ์ ๊ณต DTO
๋ ๋ฐ์ดํฐ๋ฅผ ์บก์ํํ์ฌ ์ ์กํ๋ ๊ฐ์ฒด์ด๋ฏ๋ก,record
์ ๊ฐ๊ฒฐํจ๊ณผ ๋ถ๋ณ์ฑ์ด ํฐ ์ฅ์
DTO
์ ํ๋๋ง ์ ์ํ๋ฉด ํด๋น ํ๋๋ฅผ ํฌํจํ๋ ์์ฑ์, getter ๋ฑ์ด ์๋์ผ๋ก ์ ๊ณต- ํ๋ ์ด๋ฆ ์์ฒด๊ฐ getter ์ญํ ์ ํ๋ฏ๋ก
getName()
๋์name()
๋ฉ์๋๋ฅผ ์ฌ์ฉ record
์ ํ๋๋ ๊ธฐ๋ณธ์ ์ผ๋กfinal
์ฒ๋ผ ๋์ํ๊ธฐ ๋๋ฌธ์ ๊ฐ์ฒด ์์ฑ ํ ๊ฐ ๋ณ๊ฒฝ ๋ถ๊ฐrecord
๋ฅผ ์ ์ธํ ๋๋ ํ์ํ ํ๋๋ฅผ ์์ฑ์ ํ๋ผ๋ฏธํฐ๋ก ์ ์ธ
- ๋จ์ํ ๋ฐ์ดํฐ ์ ์ก์ ๊ฒฝ์ฐ
record
๊ฐ ์ ํฉ!- ๋ณต์กํ ๋ก์ง์ด๋ ์์์ด ํ์ํ ๊ฒฝ์ฐ
class
๊ฐ ์ ํฉ!
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);
}
๊ทธ ์ด์ ๋ณด๋ค ๋น์ฆ๋์ค ๋ก์ง์ด ๋ ์ ๋ณด์ด๊ณ ๊ฐ๋ ์ฑ์๊ฒ ์์ฑ๋๋ค๋ ๊ฒ์ ํ์ธํ ์ ์๋ค!
๊ธฐ์กด์ @Where
์ด๋
ธํ
์ด์
์ด deprecated (๋ ์ด์ ์ฌ์ฉ๋์ง ์์) ๋๊ณ , ๊ทธ ๋์์ฑ
์ผ๋ก ์ฌ์ฉ๋๋ ๊ฒ์ด @SQLRestriction
์ด๋ค!
- ํน์ ์ํฐํฐ ํ๋์ ๋ํด SQL ์กฐ๊ฑด์ ์ค์ ํ๋๋ฐ ์ฌ์ฉ
- ํด๋น ํ๋์ ๋ํ ์กฐํ๋ ์์ ์ด ์ด๋ฃจ์ด์ง ๋๋ง๋ค ์ด ์ ์ฝ ์กฐ๊ฑด์ด ์๋์ผ๋ก ๋ฐ์
- ํนํ ํํฐ๋ง์ด ํ์ํ๊ฑฐ๋, ๋ ผ๋ฆฌ์ ์ผ๋ก ์ญ์ ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ธํ๊ณ ์ถ์ ๊ฒฝ์ฐ ๋ฑ์ ์ฌ์ฉ๋จ
โ ๊ทธ๋ฌ๋ @SQLRestriction
์ ๋ํ ๋ฌธ์ ์ ์ด ๋ง์ ๊ฒ ๊ฐ์, ์ฌ์ฉํ์ง ์๊ธฐ๋ก ๊ฒฐ์ ํ์์
- ์ ์ฝ ์กฐ๊ฑด์ด ๊ณ ์ ์
@SQLRestriction
์ ์ง์ ๋ ์กฐ๊ฑด์ ๋์ ์ผ๋ก ๋ณ๊ฒฝํ ์ ์์ผ๋ฏ๋ก, ์กฐ๊ฑด์ด ๊ณ ์ ๋ ์ํฉ์์๋ง ์ฌ์ฉ์ด ๊ฐ๋ฅ
- ๋ณต์กํ ํํฐ๋ง์์๋ ์ด๋ ค์ ์กด์ฌ
- ๋ฐฑ์คํผ์ค๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ ํต๊ณ๋ฅผ ๋ด๋ ๊ฒฝ์ฐ, ์ญ์ ๋ ๋ฐ์ดํฐ ์กฐํ ๋ถ๊ฐ
- Soft Delete ๋ฐฉ์์ ์ ํํ ์๋ฏธ๊ฐ ์ฌ๋ผ์ง
Optional<Post> findByIdAndDeletedAtIsNull(Long postId);
์กฐ๊ฑด๋ถ ์ฟผ๋ฆฌ ๋ฉ์๋๋ฅผ ์์ฑํ๋ ๋ฐฉ์์ ์ ํํ์๋ค.
deletedAt
์ด NULL
์ธ ๊ฒฝ์ฐ๋ง ์กฐํํ๊ธฐ ๋๋ฌธ์ ๋ช
์์ ์ด๊ณ ์ ์ฐํ ์กฐ๊ฑด ์ค์ ์ด ๊ฐ๋ฅํด์ง๋ค!
์ปฌ๋ ์ (๋ฐฐ์ด ํฌํจ)์ ์ ์ฅ ์์๋ฅผ ํ๋์ฉ ์ฐธ์กฐํด์ ๋๋ค์์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋๋ก ํด์ฃผ๋ ๋ฐ๋ณต์
- ๋ฐ์ดํฐ ์์ค๋ฅผ ์คํธ๋ฆผ์ผ๋ก ๋ณํ (
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๋ก ์์ง
// ๋ฉ์๋ ๋ ํผ๋ฐ์ค
.map(this::findOrCreateHashtag)
.map(HashtagResponseDto::from)
// ๊ธฐ์กด
.map(hashtag -> this.findOrCreateHashtag(hashtag))
.map(hashtag -> HashtagResponseDto.from(hashtag))
- Java 8์์ ๋์ ๋ ๋ฉ์๋๋ก, ์คํธ๋ฆผ์ ์์๋ค์ ๋ฆฌ์คํธ๋ก ์์งํ๋ ๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ๋ฐฉ๋ฒ
- ์คํธ๋ฆผ์์ ์์ง๋ ๋ฐ์ดํฐ๋ฅผ
List
๋ก ๋ณํํ๋ Collector
stream.collect(Collectors.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()) // ๋ฆฌ์คํธ๋ก ๋ณํ
public Chatroom toEntity(Member sender, Member receiver) {
return Chatroom.builder()
.sender(sender)
.receiver(receiver)
.build();
}
- ํด๋์ค์ ์ธ์คํด์ค๋ฅผ ๋จผ์ ์์ฑ ํ ํธ์ถ ๊ฐ๋ฅ
- ํน์ ๊ฐ์ฒด์ ์ํ๋ฅผ ๋ณํํ๊ฑฐ๋ ๊ทธ ๊ฐ์ฒด์ ๋ฐ์ ํ ๊ด๋ จ์ด ์์ ๋ ์ฃผ๋ก ์ฌ์ฉ
- ๊ฐ์ฒด์ ์ํ์ ๋ฐ๋ผ ๋์ํ๋ ๋ฉ์๋๋ฅผ ์ ์ํ ๋ ์ฌ์ฉ
- ์)
toEntity
๋ChatroomRequestDto
๊ฐ์ฒด๊ฐ ๋ง๋ค์ด์ง ํ์ ๊ทธ ๊ฐ์ฒด์ ์ ๊ทผํด ํธ์ถ๋จ
MyClass obj = new MyClass(); // ๊ฐ์ฒด๊ฐ ๋ง๋ค์ด์ ธ์ผ
obj.instanceMethod(); // ํธ์ถ ๊ฐ๋ฅ
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();
@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
์ํฐํฐ๋ค์ ์๋์ผ๋ก ์์์ฑ ์ปจํ
์คํธ์ ์ ์ฅ๋๊ฑฐ๋ ์ญ์ ๋๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ์ ๊ฒฝ์จ์ฃผ์ง ์์๋ ๋จ!
@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;
}
}
@Operation(summary = "๊ฒ์๊ธ ์กฐํ", description = "ํ๋์ ๊ฒ์๊ธ์ ์กฐํํ๋ API")
@GetMapping("/{postId}")
public CommonResponse<PostResponseDto> getPost(@PathVariable Long postId) {
return new CommonResponse<>(ResponseCode.SUCCESS, postService.getPost(postId));
}
์ฌ์ฉ์๊ฐ ์ฐธ์ฌํ ์ผ๋์ผ ์ฑํ ๋ฐฉ์ ์กฐํํ๋ 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("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 ํ๋ผ๋ฏธํฐ๋ฅผ ์ฟผ๋ฆฌ์์ ์ฌ์ฉํ ์ ์๊ฒ ํจ
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class PostControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
-
@SpringBootTest
์@AutoConfigureMockMvc
- SpringBoot ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ฑ
MockMvc
๋ฅผ ์๋ ์ค์ ํ์ฌ ํ ์คํธ ์ค ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ๋ฅผ ๋ก๋ํ๊ณ , ์ปจํธ๋กค๋ฌ์ ์ค์ ์๋ํฌ์ธํธ๋ฅผ ํธ์ถํ ์ ์๋๋ก ํจ
-
@Transactional
- ํ ์คํธ๊ฐ ๋๋๋ฉด DB ๋ณ๊ฒฝ ์ฌํญ์ ์๋์ผ๋ก ๋กค๋ฐฑํ์ฌ ํ ์คํธ ํ๊ฒฝ์ ๊นจ๋ํ๊ฒ ์ ์งํ๋ ์ญํ
-
ObjectMapper
- Java ๊ฐ์ฒด์ JSON ๊ฐ์ ๋ณํ์ ๋ด๋น
- ์์ฒญ ๋ณธ๋ฌธ์ผ๋ก ๊ฐ์ฒด๋ฅผ JSON์ผ๋ก ๋ณํํ์ฌ ์ ๋ฌํ๊ฑฐ๋, JSON ์๋ต์ ๊ฐ์ฒด๋ก ๋ณํํ ๋ ์ฌ์ฉ
@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
ํ๋ ๊ฐ์ด ์ผ์นํ๋์ง ํ์ธ
- ์๋ต JSON์
JWT๋ฅผ ์ด์ฉํ ์ธ์ฆ ๋ฐฉ์์์๋ accessToken ๊ณผ refreshToken ์ ํ์ฉํ๋ค.
- ๋ก๊ทธ์ธ ์, accessToken๊ณผ refreshToken ๋ฐ๊ธ
- ์ด๋, refreshToken์ accessToken๋ณด๋ค ํ ํฐ ๋ง๋ฃ ๊ธฐ๊ฐ์ด ๋ ๊ธธ๋ค!
- accessToken์ผ๋ก ์ธ์ฆ
- ํด๋ผ์ด์ธํธ๋ API ์์ฒญ ์ accessToken์ ์ฌ์ฉํ์ฌ ์๋ฒ์ ์ฌ์ฉ์๋ฅผ ์ธ์ฆ
- accessToken ๋ง๋ฃ ์ refreshToken ์ฌ์ฉ
-
accessToken์ด ๋ง๋ฃ๋๋ฉด, ํด๋ผ์ด์ธํธ๋ refreshToken์ ์ฌ์ฉํด ์๋ก์ด accessToken์ ์์ฒญ
-
์๋ฒ๋ DB์ ์ ์ฅ๋ refreshToken๊ณผ ๋น๊ตํ์ฌ ์ ํจํ ๊ฒฝ์ฐ, ์๋ก์ด accessToken ๋ฐ๊ธ
โ ๊ฒ์ฆ์ ์ํด์ ์๋ฒ์ refreshToken์ ๋ณ๋๋ก ์ ์ฅ์ํค๋ ๋ก์ง ํ์!
-
- refreshToken ๋ง๋ฃ ์ ์ฌ๋ก๊ทธ์ธ ํ์
- refreshToken ๋ํ ๋ง๋ฃ๋๊ฑฐ๋ ์ ํจํ์ง ์์ผ๋ฉด, ์ฌ์ฉ์๋ ๋ค์ ๋ก๊ทธ์ธํด์ผ ํจ
Header + Payload + Signature ๊ตฌ์กฐ
๋ด๋ถ ์ ๋ณด๋ฅผ ๋จ์ BASE64
๋ฐฉ์์ผ๋ก ์ธ์ฝ๋ฉํ๊ธฐ ๋๋ฌธ์ ์ธ๋ถ์์ ์ฝ๊ฒ ๋์ฝ๋ฉ์ด ๊ฐ๋ฅํ๋ค.
๋ฐ๋ผ์, ์ธ๋ถ์์ ์ด๋ํด๋ ๋๋ ์ ๋ณด๋ง์ ๋ด์์ผ ํ๋ค!
โ ํ ํฐ ๋ด๋ถ์ ๋น๋ฐ๋ฒํธ์ ๊ฐ์ ๊ฐ ์ ๋ ฅ ๊ธ์ง
-
ํ ํฐ ์์ฒด์ ๋ฐ๊ธ์ฒ๋ฅผ ํ์ธํ๊ธฐ ์ํด์ ์ฌ์ฉ
-
(๋ด๊ฐ ์ ํํ) ์ํธํ ๋ฐฉ์ : ์๋ฐฉํฅ ๋์นญํค ๋ฐฉ์์ธ
HS256
์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์ โ ์ํธํ ํค๋ฅผ ๋ฐ๋ก
application.yml
ํ์ผ์ ์ ์ฅํด๋์ด์ผ ํจ (์ ์ถ ๋ฐฉ์ง ์ํจ)
'๋๊ฐ' ๋ก๊ทธ์ธ ์ค์ธ์ง์ ๋ํ ์ํ๋ฅผ ๊ธฐ์ตํ๊ธฐ ์ํด ์ธ์ , ํ ํฐ, ์ฟ ํค๋ฅผ ์ฌ์ฉํ๋ค.
-
์๋ฒ ์ค์ฌ์ ์ธ์ฆ ๋ฐฉ์ โ ์๋ฒ์ ์ฌ์ฉ์ ์ํ๋ฅผ ์ ์ฅ
-
๋น๋ฐ๋ฒํธ์ ๊ฐ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ฟ ํค์ ์ ์ฅํ์ง ์๊ณ , ๋์ ์ ์ฌ์ฉ์์ ์๋ณ์์ธ
session Id
๋ฅผ ์ ์ฅโ
session id
๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์ ์ํตํ๋ ํ์ -
๋ณด์์ฑ์ด ๋์ ๋ฐ๋ฉด, ์ ์ฅ์๊ฐ ๊ณผ๋ถํ๋ ๊ฐ๋ฅ์ฑ ์กด์ฌ
-
ํด๋ผ์ด์ธํธ ์ธก์ ํ ํฐ ์ ๋ณด ์ ์ฅ
-
์์ฒญ ํค๋์ ์ง์ ํฌํจํ์ฌ ์ ์ก
-
์๋ฒ๋ ๋ฌด์ํ (Stateless) ๋ก ๋์
โ
Stateless
: ์๋ฒ๊ฐ ๊ฐ ์์ฒญ์ ๋ํ ์ํ๋ฅผ ์ ์ฅํ์ง ์๋ ๋ฐฉ์ -
ํ์ฅ์ฑ๊ณผ ์ฑ๋ฅ์ด ๋ฐ์ด๋์ง๋ง, ๋ณด์ ์ธก๋ฉด์์ ๊ด๋ฆฌ๊ฐ ํ์
-
์ผ์ ํ ํ ํฐ ์ ํจ๊ธฐ๊ฐ ๋์์ ํ ํฐ ๋ณด์ ๊ด๋ฆฌ ํ์
- ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ์ ์ํ ์ ๋ณด๋ฅผ ์ ์งํ๊ธฐ ์ํด ์ฌ์ฉ
- ์ฟ ํค๋ ๊ณต๊ฐ ๊ฐ๋ฅํ ์ ๋ณด๋ฅผ ์ฌ์ฉ์์ ๋ธ๋ผ์ฐ์ ์ ์ ์ฅ์ํด
- ์ฌ์ฉ์๋ฅผ ์๋ณํ ์ ์๋ ํ ํฐ (
refreshToken
) ์ด๋session ID
๊ฐ์ ์๋ณ ์ ๋ณด๋ฅผ ์ ์ฅ - ์๋ฒ์ ๋ถ๋ด์ด ์๊ณ ํด๋ผ์ด์ธํธ ์ธก์์ ์ํ๋ฅผ ์ ์งํ ์ ์์ง๋ง, ๋ณด์์ ์ทจ์ฝํ๊ณ ํด๋ผ์ด์ธํธ์ธก์ผ๋ก๋ถํฐ ์กฐ์๋ ๊ฐ๋ฅ์ฑ์ด ์กด์ฌํ๋ค๋ ๋จ์ ์กด์ฌ
- ์๋ฒ๋ ํด๋ผ์ด์ธํธ์ ๋ก๊ทธ์ธ ์์ฒญ์ ๋ํ ์๋ต์ ์์ฑํ ๋, ํด๋ผ์ด์ธํธ ์ธก์ ์ ์ฅํ๊ณ ์ถ์ ์ ๋ณด๋ฅผ ์๋ต ํค๋์ย
set-cookie
ย ์ ๋ด์ ์๋ต - ํด๋ผ์ด์ธํธ๊ฐ ์ฟ ํค๋ฅผ ์ ์ฅํ๊ณ ์ดํ ๋ชจ๋ ์์ฒญ๋ง๋ค ์ฟ ํค๋ฅผ ์๋ฒ๋ก ๋ค์ ์ ์กํ๋ ๋ฐฉ์์ผ๋ก ๋์
- ์๋ฒ๋ ์ฟ ํค์ ๋ด๊ธด ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ํด๋น ์์ฒญ์ ํด๋ผ์ด์ธํธ๊ฐ ๋๊ตฐ์ง ์๋ณ
- ์ธ์ฆ์ ๊ณผ์ ์ 'ํ ์๋น์ค์๊ฒ ์์' ํ๋ ์ธ์ฆ ๋ฐฉ์ (์: ์์ ๋ก๊ทธ์ธ)
-
์ฌ์ฉ์ ์์ฒญ
- ํด๋ผ์ด์ธํธ๋ ์ฌ์ฉ์๊ฐ ๋ฆฌ์์ค ์๋ฒ (์: Google, Facebook) ์ ์ ๊ทผํ๊ณ ์ ํ๋ ๊ฒฝ์ฐ, ์ฌ์ฉ์๋ฅผ OAuth ์ธ์ฆ ์๋ฒ๋ก ๋ฆฌ๋๋ ์ ํ์ฌ ์ ๊ทผ ๊ถํ์ ์์ฒญ
-
์ฌ์ฉ์ ์น์ธ
- ์ฌ์ฉ์๋ OAuth ์๋ฒ์์ ๋ก๊ทธ์ธํ๊ณ ์ ํ๋ฆฌ์ผ์ด์ ์ด ์์ ์ ์ ๋ณด์ ์ ๊ทผํ๋ ๊ฒ์ ์น์ธ
-
Authorization Code ๋ฐ๊ธ
- ์ธ์ฆ ์๋ฒ๋ ์ฌ์ฉ์๋ฅผ ์น์ธํ ํ, ํด๋ผ์ด์ธํธ์๊ฒ
Authorization Code
๋ฅผ ๋ฐ๊ธํ์ฌ ์ ๋ฌ - ์ด ์ฝ๋๋ ์ผํ์ฉ์ด๋ฉฐ ์งง์ ์๊ฐ ๋์๋ง ์ ํจ
- ์ธ์ฆ ์๋ฒ๋ ์ฌ์ฉ์๋ฅผ ์น์ธํ ํ, ํด๋ผ์ด์ธํธ์๊ฒ
-
accessToken ์์ฒญ
- ํด๋ผ์ด์ธํธ๋
Authorization Code
์ ํจ๊ป ์ธ์ฆ ์๋ฒ์accessToken
์ ์์ฒญ
- ํด๋ผ์ด์ธํธ๋
-
accessToken ๋ฐ๊ธ
- ์ธ์ฆ ์๋ฒ๋ ์์ฒญ์ ๊ฒ์ฆํ ํ, ํด๋ผ์ด์ธํธ์๊ฒ
accessToken
์ ๋ฐ๊ธ
- ์ธ์ฆ ์๋ฒ๋ ์์ฒญ์ ๊ฒ์ฆํ ํ, ํด๋ผ์ด์ธํธ์๊ฒ
-
๋ฆฌ์์ค ์๋ฒ ์์ฒญ
- ํด๋ผ์ด์ธํธ๋
accessToken
์ ํฌํจํด ๋ฆฌ์์ค ์๋ฒ์ ์์ฒญ์ ๋ณด๋ - ์๋ฒ๋ ํ ํฐ์ ๊ฒ์ฆํ์ฌ ์์ฒญ๋ ๋ฆฌ์์ค ์ ๊ณต
- ํด๋ผ์ด์ธํธ๋
๊ตฌ๊ธ์ ์น ์ฌ์ดํธ ์ฌ์ฉ์๊ฐ '๊ตฌ๊ธ ๋ก๊ทธ์ธ' ๊ธฐ๋ฅ์ ํตํด ๊ตฌ๊ธ์๊ฒ ์ ์กํ ๊ตฌ๊ธ ๊ณ์ ์ ๋ณด๊ฐ ์ ํจํ์ง (๊ตฌ๊ธ ์์ด๋ ๋ฐ ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ๋์ง) ๋ฅผ ํ์ธํ๋ค.
์ ํจํ๋ค๋ฉด ํด๋นํ๋ ๊ตฌ๊ธ ์ ์ ์ ๋ณด ์ค ์ผ๋ถ (์ ์ ์ด๋ฆ, ํ๋กํ ์ด๋ฏธ์ง ๋ฑ) ๋ฅผ ๋ด ์น ์ฌ์ดํธ์ ์ ๊ณตํด์ฃผ๋ '์ธ์ฆ' ๊ณผ์ ๋ง์ ์ฒ๋ฆฌํด์ฃผ๋ ๋ฐฉ์์ด๋ค!
์ฌ์ฉ์๊ฐ ๋๊ตฌ์ธ์ง ํ์ธํ๊ณ ์ฆ๋ช ํด์ฃผ๋ ๋ก๊ทธ์ธ/๋ก๊ทธ์์ ๊ฐ์ ๊ฒ
์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ ํ์ด์ง์ ์ ๊ทผํ ์ ์๋ ๊ถํ
โ ์ธ์ฆ์ด ๋จผ์ ์ด๋ฃจ์ด์ง๊ณ ๊ทธ ๋ค์ ์ธ๊ฐ๊ฐ ๋ค๋ฐ๋ฅด๊ฒ ๋จ
์ฌ์ฉ์ ์ด๋ฆ (๋๋ค์) ๊ณผ ๋น๋ฐ๋ฒํธ๋ง์ ์ด์ฉํด ๋ก๊ทธ์ธํ๋ ๋ฐฉ์์ผ๋ก ๋ก์ง์ ๊ตฌํํด๋ณด์๋ค.
- ์คํ๋ง ์ํ๋ฆฌํฐ์ ์ธ๊ฐ ๋ฐ ์ค์ ์ ๋ด๋นํ๋ ํด๋์ค
- ์ธ์ฆ์ ๊ด๋ฆฌํ๋
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 ์๋ฌ
์๋ก ๋ค๋ฅธ ๋๋ฉ์ธ์ ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ API ํธ์ถ ์ ํ๋์ด ๋ฐ์ํ๋ ์๋ฌ
์) ๋ฐฑ์๋์ 8080 ํฌํธ์ ํ๋ก ํธ์๋์ 3000 ํฌํธ
โ ํฌํธ๊ฐ ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ์๋ก ๋ค๋ฅธ ์ถ์ฒ๋ก ์ธ์๋์ด CORS ์๋ฌ๊ฐ ๋ฐ์
configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
โ ํ๋ก ํธ์๋์์ ๋ฐ์ดํฐ ๋ณด๋ผ 3000๋ฒ๋ ํฌํธ ํ์ฉ
- ์ปค์คํ
UsernamePasswordAuthentication
ํํฐ ์์ฑ (์์๋ฐ์ ์ฌ์ฉ) - ๋ก๊ทธ์ธ ์์ฒญ ์ฒ๋ฆฌํ๋ ํํฐ โ ์์ด๋, ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ์ ์ํ ์ปค์คํ ํํฐ
AuthenticationManager
๋ฅผ ์ด์ฉํ์ฌ DB์ ์ ์ฅ๋์ด ์๋ ํ์ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๊ฒ์ฆํ ๋ก์ง ์์ฑ- ๋ก๊ทธ์ธ ์ฑ๊ณต ์ JWT๋ฅผ ๋ฐํํ success ํธ๋ค๋ฌ ์์ฑ
- ์ปค์คํ
ํ ํํฐ
SecurityConfig
์ ๋ฑ๋กํด์ผ ํจ - refreshToken์ DB์ ์ ์ฅํด์ ํ ํฐ ์ฌ๋ฐ๊ธ์ด ๊ฐ๋ฅํ๋๋ก ํจ
UserDetailsService
์ปค์คํ ํด์ ๊ตฌํ- DB์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์กฐํํ๋ ๊ธฐ๋ฅ
UserDetails
์ปค์คํ ํด์ ๊ตฌํ- ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ด๋ฆฌ
UserDetailsService
์ ๋ฐ์ดํฐ๋ฅผ ๋๊ฒจ์ฃผ๋ DTO ์ญํ- ์ฌ๊ธฐ์ ์คํ๋๋
getMemberId
๋ฉ์๋๋ฅผ ์ด์ฉํ์ฌ ์ถํ ์ปจํธ๋กค๋ฌ์ ์ฌ์ฉ๋ ์ฌ์ฉ์ ํ ํฐ ์ ๋ณด ๊ฐ์ ธ์ด
-
UsernamePasswordAuthenticationFilter
๊ฐ ํธ์ถํAuthenticationManager
๋ฅผ ํตํด ์งํ -
AuthenticationManager
๋ DB์์ ์กฐํํ ๋ฐ์ดํฐ๋ฅผUserDetailsService
๋ฅผ ํตํด ๋ฐ์์ ํ์ ๊ฒ์ฆ
- JWT์ ๊ดํ ๋ฐ๊ธ๊ณผ ๊ฒ์ฆ์ ๋ด๋นํ๋ ํด๋์ค
- JWT๋ฅผ ์์ฑํ๊ณ ๊ฒ์ฆํ๋ ํต์ฌ ๋ก์ง!
-
๋ค์ด์ค๋ HTTP ์์ฒญ์ ํค๋์์ JWT ์ถ์ถ ๋ฐ ์ฌ์ฉ์ ์ธ์ฆ์ ๋ํ ์ฒ๋ฆฌ ์งํ
-
์์ฒญ ํค๋ Authorization ํค์ JWT๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ, JWT๋ฅผ ๊ฒ์ฆํ๊ณ ๊ฐ์ ๋ก
SecurityContextHolder
์ ์ธ์ ์ ์์ฑ(์ด ์ธ์ ์
STATELESS
์ํ๋ก ๊ด๋ฆฌ๋๊ธฐ ๋๋ฌธ์ ํด๋น ์์ฒญ์ด ๋๋๋ฉด ์๋ฉธ)
๋ก๊ทธ์ธ ์ฑ๊ณต ์ Access/Refresh
์ ํด๋นํ๋ ๋ค์ค ํ ํฐ ๋ฐ๊ธ โ ์ด 2๊ฐ์ ํ ํฐ์ ๋ฐ๊ธ
- accessToken: ํค๋์ ๋ฐ๊ธ ํ ํ๋ก ํธ์์ ๋ก์ปฌ ์คํ ๋ฆฌ์ง ์ ์ฅ
- refreshToken: ์ฟ ํค์ ๋ฐ๊ธ
/login
: Spring Security์์ ๊ธฐ๋ณธ์ผ๋ก ์ฌ์ฉํ๋ ๋ก๊ทธ์ธ ์๋ํฌ์ธํธ
/login
POST ์์ฒญ์ ํตํด ๋๋ค์๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํ๋ฉด, ์๋ต ํค๋์ accessToken ๊ฐ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๋จ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค!
๋ง์ฐฌ๊ฐ์ง๋ก, refreshToken ๋ํ ์ฟ ํค์ ์ฌ๋ฐ๋ฅด๊ฒ ๋ฐ๊ธ๋๋ ๊ฒ์ ํ์ธํ ์ ์๋ค!
- ์๋ฒ ์ธก
JWTFilter
์์ accessToken์ ๋ง๋ฃ๋ก ์ธํ ํน์ ํ ์ํ ์ฝ๋๊ฐ ํ๋ก ํธ์๋์๊ฒ ์๋ต - ํ๋ก ํธ ์ธก์ ์์ธ ํธ๋ค๋ฌ์์ accessToken ์ฌ๋ฐ๊ธ์ ์ํ refreshToken์ ์๋ฒ ์ธก์ผ๋ก ์ ์ก
- ์๋ฒ์์๋ refreshToken์ ๋ฐ์ ์๋ก์ด accessToken์ ์๋ต
- ์ด๋ accessToken ๊ฐฑ์ ์ refreshToken๋ ํจ๊ป ๊ฐฑ์ (โญ Refresh Rotate)
refreshToken์ ๋ฐ์ accessToken ๊ฐฑ์ ์ refreshToken๋ ํจ๊ป ๊ฐฑ์ ํ๋ ๋ฐฉ๋ฒ
Rotate ๋๊ธฐ ์ด์ ์ ํ ํฐ์ ๊ฐ์ง๊ณ ์๋ฒ์ธก์ผ๋ก ๊ฐ๋ ์ธ์ฆ์ด ๋๋ ๋ฌธ์ ๋ฐ์!
โ ์๋ฒ์ธก์์ ๋ฐ๊ธํ๋ refreshToken๋ค์ ๊ธฐ์ตํ ๋ค ๋ธ๋๋ฆฌ์คํธ ์ฒ๋ฆฌ๋ฅผ ์งํํ๋ ๋ก์ง์ ์์ฑํด์ผ ํ๋ค!
(Rotate ์ด์ ์ refreshToken์ ์ฌ์ฉํ์ง ๋ชปํ๋๋ก,,)
- refreshToken ๊ต์ฒด๋ก ๋ณด์์ฑ ๊ฐํ
- ๋ก๊ทธ์ธ ์ง์์๊ฐ ๊ธธ์ด์ง
์๋ช ์ฃผ๊ธฐ๊ฐ ๊ธด refreshToken์ ๋ฐ๊ธ ์ ์๋ฒ์ธก ์ ์ฅ์์ ์ ์ฅํ ํ ์๋ฒ์ ๊ธฐ์ต๋์ด ์๋ refreshToken๋ง ์ฌ์ฉํ ์ ์๋๋ก ํ๋ ๊ฒ์ด ์ข๋ค (์๋ฒ์ธก ์ฃผ๋๊ถ)
-
๋ฐ๊ธ์ : refreshToken์ ์๋ฒ์ธก ์ ์ฅ์์ ์ ์ฅ
-
๊ฐฑ์ ์ (Refresh Rotate) : ๊ธฐ์กด refreshToken์ ์ญ์ ํ๊ณ ์๋ก ๋ฐ๊ธํ refreshToken์ ์๋ก ์ ์ฅ
RDB ๋๋ Redis์ ๊ฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ํตํด refreshToken์ ์ ์ฅํ๋ค.
์ด๋ Redis์ ๊ฒฝ์ฐ, TTL
์ค์ ์ ํตํด ์๋ช
์ฃผ๊ธฐ๊ฐ ๋๋ ํ ํฐ์ ์๋์ผ๋ก ์ญ์ ํ ์ ์๋ค๋ ์ฅ์ ์ด ์๋ค.
CustomLogoutFilter
๋ฅผ ํตํด ๋ก๊ทธ์์ ๋ก์ง์ ๊ตฌํํ๋ค.
๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์กด์ฌํ๋ accessToken ์ญ์ ๋ฐ ์๋ฒ์ธก ๋ก๊ทธ์์ ๊ฒฝ๋ก๋ก refreshToken ์ ์ก
- refreshToken์ ๋ฐ์ Cookie ์ด๊ธฐํ (
NULL
) ํ, Refresh DB์์ ํด๋น refreshToken ์ญ์ - ์ธ์ ์ ๋ฌดํจํํ๊ณ , ์ธ์ฆ ์ ๋ณด๋ฅผ ์ ๊ฑฐ
(nickname ๊ธฐ๋ฐ์ผ๋ก ๋ชจ๋ refreshToken ์ญ์ ํ๋ ๋ก์ง ๊ตฌํ)
/logout
: Spring Security์์ ๊ธฐ๋ณธ์ผ๋ก ์ฌ์ฉํ๋ ๋ก๊ทธ์์ ์๋ํฌ์ธํธ
/logout
POST ์์ฒญ์ ๋ณด๋ผ ์, ์ฟ ํค์ ์๋ refreshToken ๊ฐ์ด ์ฌ๋ผ์ง๋ ๊ฒ์ ํ์ธํ ์ ์๋ค!
@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()));
}
- Spring Security์์ ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ ์ง์ ์ฃผ์ ํ ๋ ์ฌ์ฉํ๋ ์ด๋ ธํ ์ด์
CustomUserDetails
๋ฅผ ์ฃผ์ ํจ์ผ๋ก์จ,memberId
๋ฅผ ๊ฐ์ ธ์ ์ค
โ๏ธ ๋ก๊ทธ์ธ ์ ์๋ต ํค๋์ ๊ธฐ๋ก๋๋ accessToken์ swagger์ Authorize ๊ฐ์ ๋ฃ์ด์ ์ฌ์ฉ์ ์ธ์ฆ์ ํด์ผ ํ๋ค!
โ๏ธ ์ธ์ฆ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๋์๋ค๋ฉด, memberId
๊ฐ์ ์
๋ ฅํ์ง ์์๋ ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ์ฌ์ฉ์ ํ ํฐ์ด ํ์ํ API ์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์คํ๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
ํ ํฐ์ด ํ์ํ ๋ชจ๋ API ์์ฒญ ํ
์คํธ ์, 403 Forbidden
์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
403 Forbidden
: ์ ๊ทผ ๊ถํ์ด ์๋ ๊ฒฝ์ฐ
Swagger์์ 403 Forbidden
์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ์ฃผ์ ์์ธ์๋ ๋ฌด์์ด ์์๊น?
์ฃผ๋ก ์ธ์ฆ ๋๋ ์ธ๊ฐ์ ๋ฌธ์ ๊ฐ ์๋ ๊ฒฝ์ฐ๋ผ๊ณ ํ๋ค.
- ์์ฒญ์ ๋ณด๋ผ ๋,
Authorization
ํค๋์ JWT ํ ํฐ์ ํฌํจํ์ง ์์๊ฑฐ๋ ์๋ชป๋ ํ์์ผ๋ก ํฌํจํ ๊ฒฝ์ฐ
๐ค Swagger์ Authorize ๋ฒํผ์ ํตํด, /login
์ ์๋ต ํค๋๋ก๋ถํฐ ์ ํด์ง accessToken ๊ฐ์ ์ฌ๋ฐ๋ฅด๊ฒ ๋๊ฒผ๋ค๊ณ ์๊ฐํ๋ค!
- JWT ํ ํฐ์ด ๋ง๋ฃ๋๋ฉด ์ธ์ฆ์ด ์คํจํจ
๐ค ๋ฐฉ๊ธ ๋ก๊ทธ์ธํด์ ๋ฐ์ accessToken ๊ฐ์ด๊ธฐ ๋๋ฌธ์ ๋ง๋ฃ๋์์ ๋ฆฌ๊ฐ ์๋ค!
- ํน์ ๊ฒฝ๋ก์ ๋ํด ๊ถํ์ด ํ์ํ์ง๋ง, ์์ฒญ์ ๋ณด๋ธ ์ฌ์ฉ์์ ๊ถํ์ด ๋ถ์กฑํ ๊ฒฝ์ฐ
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 ๊ฒฝ๋ก์ ๊ฒฝ์ฐ ๋ฐ๋ก ๊ถํ ์ค์ ์ ํ์ง ์์๋ค!
- ํด๋ผ์ด์ธํธ (Swagger UI) ์ ์๋ฒ ๋๋ฉ์ธ์ด ๋ค๋ฅผ ๋,
CORS
์ค์ ์ด ์ฌ๋ฐ๋ฅด์ง ์์ผ๋ฉด ์๋ฒ๊ฐ ์์ฒญ์ ์ฐจ๋จ
๐ค ๋ก์ปฌ ํ๊ฒฝ์์ localhost:8080
์ ํตํด Swagger๋ก ํ
์คํธํ ๊ฒฝ์ฐ์ด๋ฏ๋ก, ๋๋ฉ์ธ์ด ๋ฌ๋ผ ๋ฐ์ํ๋ ์ค๋ฅ์ธ CORS
์๋ฌ๊ฐ ์์ธ์ผ ๊ฐ๋ฅ์ฑ์ ์ ๋ค!
ํด๋น ๊ฒฝ์ฐ๋ค์ด ๋ชจ๋ ์ฑ๋ฆฝํ์ง ์๋๋ฐ, ๋๋์ฒด ์ค๋ฅ์ ์์ธ์ ๋ฌด์์ผ๊น??
/** [ 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์ ์๋ต์ ์ดํด๋ณด์.
Authorization : Bearer <token>
ํํ๋ก accessToken ๊ฐ์ด ๋ค์ด์ค๊ณ ์์์ ํ์ธํ ์ ์๋ค!
์ด๋ฐ ์ํฉ์์ ๋์ ์ฝ๋๋ ํค๋์ access ํค์ ํ ํฐ์ ๊บผ๋ด์ค! ๋ผ๊ณ ์์ฒญํ๊ณ ์์ผ๋ swagger์์๋ ํ ํฐ์ ๋ํ ์ธ์ ์์ฒด๋ฅผ ํ์ง ๋ชปํ ๊ฒ์ด์๋ค.
- HTTP ํ์ค๊ณผ Spring Security์์ ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ ๋ฌํ๋ ํ์ค ๋ฐฉ์
- ๋๋ถ๋ถ์ ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ (์: Axios, Postman, Swagger ๋ฑ) ์ ๋ธ๋ผ์ฐ์ ์ ์ธ์ฆ ํ ํฐ ๊ด๋ฆฌ ๋ฐฉ์์ด
Authorization
ํค๋์ ์์กดํ๋ค๊ณ ํจ
Authorization
ํค๋ ๋์ ๋ค๋ฅธ ์ด๋ฆ์ ์ฌ์ฉํ๋ฉด, ์๋์ผ๋ก Bearer
ํ ํฐ์ ์ธ์ํ์ง ๋ชปํ๊ณ ์ธ์ฆ ์ฒ๋ฆฌ๊ฐ ๋๋ฝ๋ ๊ฐ๋ฅ์ฑ ์กด์ฌ!!
/** [ 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);
-
์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ํ๊ฒ ๊ตฌ์ถ, ํ ์คํธ ๋ฐ ๋ฐฐํฌํ ์ ์๋ ์ํํธ์จ์ด ํ๋ซํผ
-
์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ๊ทธ์ ํ์ํ ๋ชจ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ์ข ์์ฑ, ์ค์ ํ์ผ ๋ฑ์ ์ปจํ ์ด๋๋ผ๋ ๋ ๋ฆฝ๋ ํ๊ฒฝ์ ํจํค์งํ์ฌ, ์ด๋์๋ ์ผ๊ด๋๊ฒ ์คํํ ์ ์๋๋ก ํด์ค (์ด์ํ๊ฒฝ ์์กดX)
-
docker hub์์ image๋ฅผ
pull
ํ๊ณ , image ๋ฅผrun
ํ๋ฉด container๊ฐ ์คํ -
ํ์ฉ ์์ ) ์ ํ๋ฆฌ์ผ์ด์ ๋ฐฐํฌ, ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ถ,
CI/CD
ํ์ดํ๋ผ์ธ
-
์ปจํ ์ด๋๋ฅผ ๋ง๋ค๊ธฐ ์ํ ํ ํ๋ฆฟ
-
์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ ์ ์๋ ํ๊ฒฝ(์ด์ ์ฒด์ , ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ์ ํ๋ฆฌ์ผ์ด์ ํ์ผ ๋ฑ)์ ํฌํจํ๋ ์ฝ๊ธฐ ์ ์ฉ ํ์ผ ์์คํ
- ๋์ปค ์ด๋ฏธ์ง์๋ ์ ํ๋ฆฌ์ผ์ด์ ์คํ์ ํ์ํ ์ํํธ์จ์ด์ ํ๊ฒฝ์ด ๋ชจ๋ ๋ค์ด ์์ผ๋ฉฐ, ์ด๋ฅผ ํตํด ์ด๋์๋ ๋์ผํ ํ๊ฒฝ์์ ์ผ๊ด๋๊ฒ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ ์ ์์
-
์์) MySQL ์ด๋ฏธ์ง
-
mysql:5.7
๊ณผ ๊ฐ์ Docker ์ด๋ฏธ์ง๋ MySQL ์๋ฒ๋ฅผ ์คํํ ์ ์๋ ํ๊ฒฝ์ ์ ๊ณตํ๋ ํ ํ๋ฆฟ -
docker pull mysql:5.7
: 'MySQL ์ด๋ฏธ์ง๋ฅผ Docker Hub ์์ ๋ค์ด๋ฐ๋๋ค' ๋ ์๋ฏธ
-
-
์ด๋ฏธ์ง๋ฅผ ์คํํ์ฌ ๋ง๋ ์ค์ ์คํ ์ค์ธ ํ๊ฒฝ
-
์ปจํ ์ด๋๋ ๋ ๋ฆฝ์ ์ด๊ณ ๊ฒฉ๋ฆฌ๋ ํ๊ฒฝ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํ
-
์์) MySQL ์ปจํ ์ด๋
-
mysql:5.7
์ด๋ฏธ์ง๋ฅผ ์คํํ์ฌ MySQL ์๋ฒ๋ฅผ ์๋์ํค๋ ์ปจํ ์ด๋๋ฅผ ์์ฑ -
docker run -d --name mysql-container mysql:5.7
:mysql:5.7
์ด๋ฏธ์ง๋ฅผ ์คํํ์ฌmysql-container
์ด๋ผ๋ ์ด๋ฆ์ MySQL ์ปจํ ์ด๋ ๋ง๋ค๊ธฐ
-
-
์ฌ๋ฌ ์ปจํ ์ด๋๊ฐ ์๋ก ํต์ ํ ์ ์๋๋ก ํด์ฃผ๋ ๊ฐ์ ๋คํธ์ํฌ
-
๊ธฐ๋ณธ์ ์ผ๋ก 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): ์ฌ๋ฌ ์ปจํ ์ด๋๊ฐ ์๋ก ํต์ ํ ์ ์๋๋ก ํด์ฃผ๋ ๊ฐ์ ๋คํธ์ํฌ (ํ ์ด๋ธ ์ฐ๊ฒฐ)
๐ ๋์ปค ์ด๋ฏธ์ง๋ ์ค๋น๋ ์ํ์ด๊ณ , ๋์ปค ์ปจํ ์ด๋๋ ์ด ์ค๋น๋ ์ด๋ฏธ์ง๋ฅผ ์ค์ ๋ก ์คํํ ๊ฒฐ๊ณผ
Gradle ํญ์์ Tasks-build-bootJar ์คํ โ build/libs ๊ฒฝ๋ก์ jar ํ์ผ ์์ฑ โ Dockerfile ์ค์
-
Docker ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ธฐ ์ํ ์ค์ ํ์ผ
-
1๊ฐ์ ์ปจํ ์ด๋ ์์ฑํ๋ ํ์ผ
-
์ ํ๋ฆฌ์ผ์ด์ ์ฝ๋, ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ํ๊ฒฝ ๋ณ์, ์ค์ ํ์ผ ๋ฑ์ ํฌํจํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํ ํ๊ฒฝ์ ๋ํ๋
-
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
- Compose ํ์ผ ๋ด์์ ๊ฐ๊ฐ์ ์ปจํ ์ด๋๋ฅผ ์ ์ํ๋ ๋ถ๋ถ
- ๊ฐ ์๋น์ค๋ ๊ฐ๋ณ ์ปจํ
์ด๋๋ก ์คํ๋๋ฉฐ,
database
,application
๊ณผ ๊ฐ์ ์ด๋ฆ์ ๊ฐ์ง ์๋น์ค๋ค์ ์ ์
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
์คํจ ์ ์ฌ์๋ ํ์๋ฅผ ์ค์
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
์๋น์ค์ ๋์ผํ ๋คํธ์ํฌ์ ์ํ๊ฒ ๋์ด ์๋ก ํต์ ํ ์ ์์
networks:
network:
driver: bridge
- ์ฌ์ฉ์ ์ ์ ๋คํธ์ํฌ๋ฅผ ์ ์
bridge
๋ ๊ธฐ๋ณธ Docker ๋คํธ์ํฌ ๋๋ผ์ด๋ฒ๋ก, ์ด ๋คํธ์ํฌ์ ์ฐ๊ฒฐ๋ ์ปจํ ์ด๋๋ ์๋ก ํต์ ํ ์ ์์
โ ์ปจํ ์ด๋ ๊ฐ ์ฐ๊ฒฐ์ ์ํด ๋คํธ์ํฌ๋ ํ์!
connection refused
๋ฌธ์ Communications link failure
๋ฌธ์
- MySQL ์๋ฒ๊ฐ ์คํ๋์ง ์์ ๋๋ ์ฐ๊ฒฐ ์ค๋น๊ฐ ๋์ง ์์
- ์๋ชป๋ ์ฐ๊ฒฐ ์ ๋ณด (ํธ์คํธ, ํฌํธ, ์ฌ์ฉ์๋ช , ๋น๋ฐ๋ฒํธ ๋ฑ)
๋ ๋ฌธ์ ๋ชจ๋ MySQL ์๋ฒ์์ ์ฐ๊ฒฐ์ด ์คํจํ์ ๋ ๋ฐ์ํ๋ ๋ฌธ์ ๋ผ๊ณ ํ๋ค..
โ ์ฒซ๋ฒ์งธ ์์ธ์ผ๋ก๋,
ํ์ฌ ๋์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋น๋ฐ๋ฒํธ์ $
๊ฐ ํฌํจ๋์ด ์๋๋ฐ, yml
์ ํ๊ฒฝ๋ณ์๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๋ฐ๋ก ์์ฑํ๋ค๋ณด๋ ํด๋น ๋ฌธ์๋ฅผ ์ธ์ํ์ง ๋ชปํด ์ฐ๊ฒฐ ๊ฑฐ๋ถ ํ์์ด ๋ฐ์ํ์๋ค.
โ ๋๋ฒ์งธ ์์ธ์ผ๋ก๋,
depends_on
๋ง ์ฌ์ฉํ๊ณ Healthcheck
๋ ํด์ฃผ์ง ์์๋ค.
application
์๋น์ค๊ฐ database
์๋น์ค์ ์ฐ๊ฒฐํ๋ ค๊ณ ์๋ํ ๋, MySQL ์๋ฒ๊ฐ ์์ง ์์ ํ ์์๋์ง ์์๊ฑฐ๋ ์ค๋น๋์ง ์์ ์ํ์ผ ์ ์๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ํ์ธํด์ฃผ๋ ๋ก์ง์ด ํ์ํ๋ค๊ณ ํ๋ค!
depends_on
: ์๋น์ค๊ฐ ์คํ๋์๋์ง๋ง ํ์ธ, ์คํ ์์๋ฅผ ๋ณด์ฅํ์ง๋ง ์๋น์ค๊ฐ ์ค์ ๋ก ์ค๋น๋์๋์ง๋ ๋ณด์ฅํ์ง ์์healthcheck
: ์๋น์ค๊ฐ ์ ์์ ์ผ๋ก ์๋ํ๋์ง (์ฆ, MySQL์ด ์ค๋น๋์๋์ง) ํ์ธํ๋ ๋ฐ ์ฌ์ฉ
depends_on
๋ง ์ฌ์ฉํ์ ๊ฒฝ์ฐ, database
์๋น์ค๊ฐ ์คํ ์ค์ธ์ง! ์ค๋น ์ํ์ธ์ง! ํ์ธํ ๋ฐฉ๋ฒ์ด ์๋ค..
thanks to.. ์ต์์ง (
@choiseoji
)
- ํ๊ฒฝ๋ณ์
.env
ํ์ผ์ ๋ง๋ค์ด์ฃผ์๋ค! Healthcheck
๊ธฐ๋ฅ์ ์ถ๊ฐํด์ฃผ์๋ค!
-
์ปจํ ์ด๋๊ฐ ์ ์์ ์ผ๋ก ๋์ํ๋์ง ์ฃผ๊ธฐ์ ์ผ๋ก ๊ฒ์ฌํ๋ ๊ธฐ๋ฅ
-
์ปจํ ์ด๋๊ฐ ์ ์์ ์ผ๋ก ์ค๋น๋์์ ๋๋ง ๋ค๋ฅธ ์๋น์ค๊ฐ ์์๋๋๋ก ํ ์ ์์
-
์์)
database
์ปจํ ์ด๋ ์คํ OK!! ๐application
์ปจํ ์ด๋ ์คํ ์์!!
Docker ์ฐ๊ฒฐ ์๋๊ณ , 8080
๋ ๋ค ์ ๋ด์ด.
๊ทธ๋์ ์ด์ API ๋ฆฌํฉํ ๋งํ๊ณ ์คํ๋ง๋ถํธ run
ํ๋๋ฐ Access denied for user 'root'@'172.18.0.1'
์๋ฌ๊ฐ ๋จ๋ค?
ํด๋น ์ค๋ฅ๋ MySQL ์๋ฒ์์ root ์ฌ์ฉ์๊ฐ IP ์ฃผ์ 172.18.0.1์์ ์ ์์ ๊ฑฐ๋ถ๋นํ๋ค๋ ๊ฒ์ ์๋ฏธํ๋ค..
MySQL์์ root ์ฌ์ฉ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ก์ปฌ์์๋ง ์ ์์ ํ์ฉํ๋๋ก ์ค์ ๋๋ค๊ณ ํ๋ค.
์ฆ, root ์ฌ์ฉ์๊ฐ localhost
๋๋ 127.0.0.1
์์๋ง ์ ์ํ ์ ์๋๋ก ์ ํ๋์ด ์๋ค๋ ์๋ฏธ์ด๋ค.
์ด๋ฌํ ์ํฉ์์ Docker ๋คํธ์ํฌ ๋ด ๋ค๋ฅธ ์ปจํ ์ด๋ (์ธ๋ถ IP) ์์ ์ ์์ ์๋ํ๋ ์ํฉ์ด๊ธฐ ๋๋ฌธ์ ์ ๊ทผ์ ์ฐจ๋จํ๋ ๊ฒ์ด์๋ค!
root ์ฌ์ฉ์๊ฐ ๋ค๋ฅธ IP์์๋ ์ ์ํ ์ ์๋๋ก ๊ถํ์ ์์ ํด์ผ ํ๋ค!
- MySQL ์ปจํ ์ด๋์ ์ ์
docker exec -it <mysql-container-name> mysql -u root -p
- root ์ฌ์ฉ์์ ๋ํด ์ธ๋ถ ํธ์คํธ์์์ ์ ์์ ํ์ฉํ๋๋ก ๊ถํ ๋ณ๊ฒฝ
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'your_password' WITH GRANT OPTION;
FLUSH PRIVILEGES;
โ ์ด๋, DB_URL ์ localhost
๋ก ๋ค์ ์์ ํ๊ธฐ
EC2 = Elastic Compute Cloud
AWS์์ ์๊ฒฉ์ผ๋ก ์ ์ดํ ์ ์๋ ๊ฐ์์ ์ปดํจํฐ ํ ๋๋ฅผ ๋น๋ฆฌ๋ ๊ฒ์ด๋ค.
EC2๋ฅผ ํ๋์ ์ปดํจํฐ๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค!
์๋ฒ๋ฅผ ๋ฐฐํฌํ๊ธฐ ์ํด์๋ ์ปดํจํฐ๊ฐ ํ์ํ๋ค. ์ด๋ ๋์ ์ปดํจํฐ์์ ์๋ฒ๋ฅผ ๋ฐฐํฌํด์ ๋ค๋ฅธ ์ฌ์ฉ์๋ค์ด ์ธํฐ๋ท์ ํตํด ์ ๊ทผํ ์ ์๊ฒ ๋ง๋ค ์๋ ์๋ค. ํ์ง๋ง ๋ด ์ปดํจํฐ๋ก ์๋ฒ๋ฅผ ๋ฐฐํฌํ๋ฉด 24์๊ฐ ๋์ ์ปดํจํฐ๋ฅผ ์ผ๋์ผํ๋ค. ๋ํ ์ธํฐ๋ท์ ํตํด ๋ด ์ปดํจํฐ์ ์ ๊ทผํ ์ ์๊ฒ ๋ง๋ค๋ค ๋ณด๋ ๋ณด์์ ์ผ๋ก๋ ์ํํ ์ ์๋ค.
์ด๋ฌํ ๋ถํธํจ ๋๋ฌธ์ ๋ด๊ฐ ๊ฐ์ง๊ณ ์๋ ์ปดํจํฐ๋ฅผ ์ฌ์ฉํ์ง ์๊ณ , AWS EC2๋ผ๋ ์ปดํจํฐ๋ฅผ ๋น๋ ค์ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค!
EC2 ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ฉด, ์ ํํ AMI์ ๋ฐ๋ผ ์ด์์ฒด์ , CPU, RAM ๋ฑ์ด ๋ฏธ๋ฆฌ ๊ตฌ์ฑ๋ ์ปดํจํฐ๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ๋๋ค!
์ธ์คํด์ค๋ฅผ ๋ง๋ค ๋, ์ฌ๋ฌ ์ธ์คํด์ค ์ ํ ๊ฐ์ด๋ฐ ํ๋๋ฅผ ๊ณ ๋ฅด๊ณ ์ฌ์ด์ฆ ๋ฑ์ ๊ณ ๋ฅธ๋ค. ์ด๋ฌํ ๊ณผ์ ์ ํตํด ๋ด๊ฐ ๋ง๋ค ๊ฐ์ ์๋ฒ์ ๋ชฉ์ ์ ๋ฐ๋ผ์ ํนํ๋ ์๋ฒ๋ฅผ ๋ง๋ค ์ ์๋ค.
AMI = Amazon Machine Image
๋ด๊ฐ ์ ํํ ์๋ฒ ํนํ ์ต์ ์ ๋ชจ์๋ ๊ฒ์ผ๋ก, ์ธ์คํด์ค ์์ฑ ์ ์ ๋ฏธ๋ฆฌ ๊ตฌ์ฑ๋๋ ์ด๋ฏธ์ง ํ์ผ์ด๋ค.
EC2 ์ธ์คํด์ค๋ฅผ ์์ํ ๋ ํ์ํ ์ ๋ณด๋ฅผ ํฌํจํ๋ค.
โ ์ด์ ์ฒด์ , ์ ํ๋ฆฌ์ผ์ด์ ์๋ฒ, ์ธ์ด ๋ฐํ์, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฑ ์ธ์คํด์ค์์ ์คํ๋๋ ๋ชจ๋ ์ํํธ์จ์ด์ ์ค์ ์ด ํฌํจ
EC2 ์ธ์คํด์ค์ ์ ์ํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ ์ํธํ๋ ํ์ผ์ด๋ค.
๋ฐ๊ธ๋ฐ์ ํ๋ผ์ด๋น ํค๋ฅผ ์ด์ฉํ์ฌ ์ธ์คํด์ค์ ์ ๊ทผํ ์ ์๊ธฐ ๋๋ฌธ์ ํค๋ฅผ ์์ ํ๊ฒ ๋ณด๊ดํ๋ ๊ฒ์ด ์ค์ํ๋ค!
EBS = Elastic Block Storage
EBS๋, ํด๋ผ์ฐ๋์์ ์ฌ์ฉํ๋ ๊ฐ์ ํ๋๋์คํฌ์ด๋ค.
EC2 ์ธ์คํด์ค๊ฐ ์ฐ์ฐ(CPU, ๋ฉ๋ชจ๋ฆฌ)์ ๊ดํ ์ฒ๋ฆฌ๋ฅผ ํ๋ค๊ณ ํ๋ฉด, ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ์ญํ ์ ๋ฐ๋ก EBS๊ฐ ํ๋ค๊ณ ๋ณด๋ฉด ๋๋ค!
EBS ๋ณผ๋ฅจ์ด๋, EBS๋ก ์์ฑํ ๋์คํฌ ํ๋ํ๋๋ฅผ ๋ปํ๋ ์ ์ฅ ๋จ์๋ฅผ ๋งํ๋ค.
์ฝ๊ฒ ๋งํด, ์๋์ฐ์ C๋๋ผ์ด๋ธ, D๋๋ผ์ด๋ธ๋ ๊ฐ๊ฐ์ ๋์คํฌ์ด๋ฉฐ EBS ๋ณผ๋ฅจ์ด๋ค!
EC2 ์ธ์คํด์ค์ ํ์ฉ๋๋ ์ธ๋ฐ์ด๋, ์์๋ฐ์ด๋ ํธ๋ํฝ์ ์ ์ดํ๋๊ฐ์ ๋ฐฉํ๋ฒฝ์ด๋ค.
์ฆ, ์ฐ๊ฒฐ๋ ๋ฆฌ์์ค์ ๋๋ฌํ๊ณ ๋๊ฐ ์ ์๋ ํธ๋ํฝ์ ์ ์ดํ๋ค.
โญ ์ธ๋ฐ์ด๋
-
์ธ๋ถ์์ ์ธ์คํด์ค์ ์ ๊ทผํ๋ ํธ๋ํฝ์ ๋ํ ํ์ฉ ๋ฒ์ ์ ์ด
-
ํด๋ผ์ด์ธํธ๊ฐ ์์ ์ ์๋ฒ ๋ฐ์ดํฐ์ ๋ค์ด์ฌ ์ ์๋ ๊ท์น
-
๊ธฐ๋ณธ์ ์ผ๋ก ์ธ๋ฐ์ด๋ ๊ท์น์ ๋ชจ๋ ํฌํธ๋ฅผ ๋ซ๋ ๊ฒ์ ์ ์
โญ ์์๋ฐ์ด๋
-
์๋ฒ์์ ์ธ๋ถ๋ก ๋๊ฐ๋ ํธ๋ํฝ์ ๋ํ ํ์ฉ ๋ฒ์ ์ ์ด
-
๊ธฐ๋ณธ์ ์ผ๋ก ๋ชจ๋ ์์๋ฐ์ด๋ ํธ๋ํฝ์ ํ์ฉ
์ค์ ๋ฉ๋ชจ๋ฆฌ RAM์ด ๊ฐ๋ ์ฐผ์ง๋ง ๋ ๋ง์ ๋ฉ๋ชจ๋ฆฌ๊ฐ ํ์ํ ๋ ๋์คํฌ ๊ณต๊ฐ์ ์ด์ฉํ์ฌ ๋ถ์กฑํ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋์ฒดํ ์ ์๋ ๊ณต๊ฐ์ ์๋ฏธํ๋ค.
EC2 ํ๋ฆฌํฐ์ด๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ RAM์ด 1GB์ด๊ธฐ ๋๋ฌธ์ ๋น๋๋ ์คํ์ ์งํํ๋ค ์ปดํจํฐ๊ฐ ๋ฉ์ถ ์ ์๋ค!
์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ์ฌ์ฉํ๋ ๊ฒ์ด ์ค์ ๋ฉ๋ชจ๋ฆฌ ์ค์ ์ด๋ผ๊ณ ํ๋ค.
์ฒ์ EC2 ์ธ์คํด์ค๋ฅผ ๋ง๋ค๋ฉด ํผ๋ธ๋ฆญ IPv4 ์ฃผ์์ ํ๋ผ์ด๋น IPv4 ์ฃผ์๊ฐ ํ ๋น๋๋ค.
-
์ธํฐ๋ท ์์์ ๊ฐ๊ฐ์ธ์ ๋ก์ปฌ ๋คํธ์ํฌ๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด ISP์์ ์ ๊ณตํ๋ IP ์ฃผ์
-
์ธ๋ถ์ ๊ณต๊ฐ๊ฐ ๋์ด์์ด์ ๋ค๋ฅธ ์ธํฐ๋ท ์ฌ์ฉ์๋ค์ด ๋์๊ฒ ์ ์ ํ ์ ์๋ค.
SSH ์ ์ ์ ์ธ์คํด์ค์ ํผ๋ธ๋ฆญ ์ฃผ์๋ฅผ ํตํด ์ธ์คํด์ค ๋ด๋ถ๋ก ์ ์ํ ์ ์๋ค.
- ์ธ๋ถ์์๋ ์ ์ํ ์ ์๋ ๋คํธ์ํฌ ๋ง
์ฐ๋ฆฌ๋ ์ด ์ฃผ์์ ์ง์ ์ ์ผ๋ก๋ ์ ๊ทผํ์ง ๋ชปํ๋ค. ์ค์ง ์ธ๋ถ๋ก ์ด๋ ค์๋ ํผ๋ธ๋ฆญ IP๋ฅผ ํตํด์๋ง ์ธ์คํด์ค์ ์ ๊ทผํ ์ ์๋ค!
ํผ๋ธ๋ฆญ IP๋ง์ผ๋ก๋ EC2 ์ธ์คํด์ค๋ฅผ ์ฌ์ฉํ๋๋ฐ๋ ๋ณ ๋ฌธ์ ๊ฐ ์์ด๋ณด์ด๋๋ฐ ํ๋ ฅ์ ์ฃผ์๋ ์ ์๋๊ฒ์ผ๊น?
AWS๋ ์ธ์คํด์ค๊ฐ ์ฒ์ ์์ฑ๋๊ฑฐ๋, ์ค์ง ํ ๋ค์ ์์ํ ๋๋ง๋ค ํผ๋ธ๋ฆญ IP๋ฅผ ์ฌํ ๋นํ๋ค. IP ์ฃผ์๊ฐ ๋งค๋ฒ ๋ฐ๋๊ฒ ๋๋ ๊ฒ์ด๋ค!
์๋น์ค ์ด์์ ์์ ์ฑ์ ์ํด ์ค์ง ํ ์ฌ์์๋ง๋ค ๋ฐ๋์ง ์๋ ๊ณ ์ ๋ IP ์ฃผ์๊ฐ ํ์์ ์ด๋ค!
RDS = Relational Database Service
๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ ๊ณตํ๋ AWS์ ์๋น์ค์ด๋ค.
RDS๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ, AWS์์ ๋ชจ๋ ๊ฒ์ ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ถ๋ถ์ ๋ํด ์ ๊ฒฝ์ ์ฐ์ง ์๊ณ ๊ฐ๋ฐ์ ์งํํ ์ ์๋ค๋ ์ฅ์ ์ด ์๋ค!
VPC = Virtual Private Cloud
๋ฌผ๋ฆฌ์ ์ผ๋ก ๊ฐ์ ํด๋ผ์ฐ๋ ์์ ์์ง๋ง, ๋ณด์์์ ๋ชฉ์ ์ ์ํด ๋ ผ๋ฆฌ์ ์ผ๋ก ๋ค๋ฅธ ํด๋ผ์ฐ๋์ธ ๊ฒ์ฒ๋ผ ๋์ํ๋๋ก ๋ง๋ ๊ฐ์ ํด๋ผ์ฐ๋ ํ๊ฒฝ์ด๋ค.
VPC๋ณ๋ก ๋ค๋ฅธ ๋คํธ์ํฌ ์ค์ ์ ํ ์ ์๊ณ , ๋ ๋ฆฝ๋ ๋คํธ์ํฌ์ฒ๋ผ ์๋ํ๋ค.
// ํ
์คํธ ์๋ตํ๊ณ ์คํ๋ง ์ ํ๋ฆฌ์ผ์ด์
๋น๋
./gradlew clean build -x test
// ๋์ปค ์ด๋ฏธ์ง๋ฅผ ๋น๋ํ๊ณ ์์ฑํ๋ ๋ช
๋ น์ด
docker build --platform linux/amd64 -t [๋์ปค์์ด๋]/[๋ฆฌํฌ์งํ ๋ฆฌ๋ช
]
// ๋์ปค ํ๋ธ์ ์ด๋ฏธ์ง ์ฌ๋ฆฌ๊ธฐ
docker push [๋์ปค์์ด๋]/[๋ฆฌํฌ์งํ ๋ฆฌ๋ช
]
docker build
๋ ํ์ฌ ๋๋ ํ ๋ฆฌ์ ์๋ Dockerfile ๊ธฐ๋ฐ์ผ๋ก ์ด๋ฏธ์ง ์์ฑ
โ ๋ด ์คํ๋ง๋ถํธ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ ์ ์๋ ํ๊ฒฝ๊ณผ ํ์ผ(JAR ํ์ผ)์ด ๋ค์ด์๋ ์ด๋ฏธ์ง
// ํจํค์ง ์
๋ฐ์ดํธ
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
์ผ๋ก ๋งคํ
DB_URL=jdbc:mysql://{RDS ์๋ํฌ์ธํธ ์ฃผ์}:3306/{RDS ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๋ฆ}
DB_USERNAME=
DB_PASSWORD=
โ ํผ๋ธ๋ฆญ ์ฃผ์๋ฅผ ํตํด ๋ค์ด๊ฐ๋ณด๋ฉด ์ฌ๋ฐ๋ฅด๊ฒ ๋จ๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค!
ํฌ์คํธ๋งจ์ ํตํด API ํ
์คํธ๋ฅผ ์งํํด๋ณด์๋๋ฐ 401 ์๋ฌ
๊ฐ ๋ฐ์ํ์๋ค.
signup
API์ ๊ฒฝ์ฐ, ์คํ๋ง ์ํ๋ฆฌํฐ์์ permitAll ์ค์ ์ ํด์ฃผ์๋๋ฐ๋ ๋ถ๊ตฌํ๊ณ ์ 401 ์๋ฌ
๊ฐ ๋ฐ์ํ ๊น? ์๋ต ํํ๋ ์ด์ํ๋ค..
์ด๊ฑฐ ์ ์ด๋ฌ๋ ๊ฑธ๊น์์ค..๐ฅฒ
๋ํ๋ ) validation ํ๊ธฐ ์ ์ ์์ธ๊ฐ ๋ฐ์ํ ๋๋์ด๋ค.. ํํฐ์ชฝ์ ๋ฌธ์ ๊ฐ ์๋๊ฑฐ๋ฉด ํค๋์ ํ ํฐ์ ๋ฃ์๋ค๋๊ฐ.. ํค๋๋ฅผ ํ์ธํด๋ด๋ผ
๋ค ๋ง์ต๋๋ค. ์ ๋ ์์ฒญ ํค๋์ ๊ธฐ์กด์ ํ ์คํธํ๋ Authorization ํ๋๋ฅผ ๊ทธ๋๋ก ๋ฃ๊ณ ํ์๊ฐ์ ์์ฒญ์ ๋ณด๋ด๊ณ ์์์ต๋๋ค..
์ด๊ฑธ ๋นผ๋๊น ๋๋ฌด ์๋๋๋ผ๊ตฌ์..
๋ฐฐํฌ๊ฐ ์๋์๋ค๋.. ๋คํ์ ๋๋ค ํ๐ฅฒ