๐ก
1. ํธ๋์ญ์ ์ฒ๋ฆฌ์ ๋ํ ๊ฐ๋ ์ ์ค๋ช ํ ์ ์๋ค.
2. ๋ํฐ ์ฒดํน ๊ฐ๋ ๊ณผ ์์์ฑ ์ปจํ ์คํธ์ ํน์ง์ ์ค๋ช ํ ์ ์๋ค.
Article ํด๋์ค(์ํฐํฐ) ์ฝ๋ ์ถ๊ฐ ํ๊ธฐ - 1
// ๋ฐ๋์ ๊ธฐ๋ณธ ์์ฑ์๊ฐ ์์ด์ผ ๋๋ค.
@Entity(name = "tb_article")
@NoArgsConstructor // ๊ธฐ๋ณธ ์์ฑ์
@Data
public class Article {
// ํน์ ์์ฑ์์๋ง ๋น๋ ํจํด์ ์ถ๊ฐํ ์ ์๋ค.
@Builder
public Article(String title, String content) {
this.title = title;
this.content = content;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //db๋ก ์์
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "title", nullable = false) // not null
private String title;
@Column(name = "content", nullable = false) // not null
private String content;
// ๊ฐ์ฒด์ ์ํ ๊ฐ ์์
public void update(String title, String content) {
// ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ๋์ ํด์ผ ํจ
// ์ฆ, ๋ฐ์ดํฐ๊ฐ ์ํฐํฐ์ ์ ์ฅ๋๊ธฐ ์ ์ ๋ฐ๋์ ๊ฒ์ฆ
if(title == null || title.trim().isEmpty()) {
throw new Exception400("์ ๋ชฉ์ null ์ด๊ฑฐ๋ ๋น ๋ฌธ์์ด์ผ ์ ์์ต๋๋ค.");
}
if(content == null || content.trim().isEmpty()) {
throw new Exception400("๋ด์ฉ์ null ์ด๊ฑฐ๋ ๋น ๋ฌธ์์ด์ผ ์ ์์ต๋๋ค.");
}
this.title = title;
this.content = content;
}
}
๋๋ฉ์ธ ๋ชจ๋ธ - ํ์ค ์ธ๊ณ์ ์ค์ํ ๊ฐ๋ ์ ์ฝ๋๋ก ๋ํ๋ธ ๊ฒ (๊ฒ์๊ธ, ์ฌ์ฉ์, ๋๊ธ, ์ฃผ๋ฌธ, ์ํ)
๊ฐ์ฒด ์ค์ค๋ก ์์ ์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋๋ก ํ๋ค - ์์ ์ ๋ฐ์ดํฐ์ ํ๋์ ์ฑ ์์ ์ง๋ค.
application-dev.yml ์์
server:
servlet:
encoding:
charset: utf-8 # ์์ฒญ ๋ฐ ์๋ต์ UTF-8 ์ธ์ฝ๋ฉ์ ์ฌ์ฉํ์ฌ ํ๊ธ ๋ฐ ํน์๋ฌธ์๊ฐ ๊นจ์ง์ง ์๋๋ก ์ค์
force: true # ๊ฐ์ ๋ก UTF-8 ์ธ์ฝ๋ฉ์ ์ ์ฉ, ํด๋ผ์ด์ธํธ๊ฐ ๋ค๋ฅธ ์ธ์ฝ๋ฉ์ ์์ฒญํ๋๋ผ๋ ๋ฌด์ํ๊ณ UTF-8์ ์ฌ์ฉ
port: 8080 # ์๋ฒ๊ฐ 8080 ํฌํธ์์ ์คํ๋๋๋ก ์ค์
spring:
mustache:
servlet:
expose-session-attributes: true # Mustache ํ
ํ๋ฆฟ์์ ์ธ์
์์ฑ์ ์ ๊ทผํ ์ ์๋๋ก ํ์ฉ
expose-request-attributes: true # Mustache ํ
ํ๋ฆฟ์์ ์์ฒญ ์์ฑ์ ์ ๊ทผํ ์ ์๋๋ก ํ์ฉ
datasource:
url: jdbc:mysql://localhost:3306/jpa_demo?useSSL=false&serverTimezone=Asia/Seoul&useLegacyDatetimeCode=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: asd123
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ธฐ๋ณธ ๋น๋ฐ๋ฒํธ (๋น์ด ์์)
h2:
console:
enabled: true # H2 ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฝ์์ ํ์ฑํํ์ฌ ๋ธ๋ผ์ฐ์ ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ด๋ฆฌํ ์ ์๋๋ก ํจ
#sql:
#init:
#data-locations:
#- classpath:db/data.sql # ์ ํ๋ฆฌ์ผ์ด์
์ด๊ธฐํ ์ ์คํํ ๋ฐ์ดํฐ ์ฝ์
SQL ํ์ผ์ ๊ฒฝ๋ก (data.sql)
jpa:
hibernate:
ddl-auto: update # ์ ํ๋ฆฌ์ผ์ด์
์ด ์์๋ ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ์ ์๋์ผ๋ก ์์ฑ
show-sql: true # Hibernate๊ฐ ์คํํ๋ SQL ์ฟผ๋ฆฌ๋ฅผ ์ฝ์์ ์ถ๋ ฅ
properties:
hibernate:
format_sql: true # ์ถ๋ ฅ๋๋ SQL ์ฟผ๋ฆฌ๋ฅผ ํฌ๋งทํ
ํ์ฌ ์ฝ๊ธฐ ์ฝ๊ฒ ์ถ๋ ฅ
defer-datasource-initialization: true # ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ๊ฐ ์ง์ฐ๋๋๋ก ์ค์ ํ์ฌ JPA ์ค์ ํ์ ๋ฐ์ดํฐ ์ด๊ธฐํ
output:
ansi:
enabled: always # ์ฝ์ ์ถ๋ ฅ ์ ANSI ์์์ ํญ์ ์ฌ์ฉํ๋๋ก ์ค์ (์์์ ํตํด ๋ก๊ทธ๋ฅผ ๋ ์ฝ๊ฒ ๊ตฌ๋ถ ๊ฐ๋ฅ)
logging:
level:
'[com.example.class_blog_jpa_v1]': DEBUG # ํน์ ํจํค์ง(com.tenco.blog_jpa_step1) ์์ค์์ DEBUG ๋ ๋ฒจ๋ก ๋ก๊น
์ ์ค์
BlogService ํด๋์ค์ ์์ ๊ธฐ๋ฅ๊ณผ ํธ๋์ญ์ ์ฒ๋ฆฌ - 2
์์ ๊ธฐ๋ฅ์ @Transactional ์ฒ๋ฆฌ ํ๊ธฐ
JpaRepository ๋ฉ์๋์ธ save()๋ delete()๋ฅผ ์ง์ ์ฌ์ฉ ํ์์. ์ด ๋ฉ์๋๋ค์ ์ด๋ฏธ ํธ๋์ญ์ ์ฒ๋ฆฌ๋์ด ์์ต๋๋ค. ๋ฐ๋ผ์ ์๋น์ค ๊ณ์ธต์์ ์ถ๊ฐ๋ก ํธ๋์ญ์ ์ ์ ์ธํ ํ์๊ฐ ์์์.
@RequiredArgsConstructor
@Service // IoC (๋น์ผ๋ก ๋ฑ๋ก)
public class BlogService {
//@Autowired // DI <-- ๊ฐ๋ฐ์๋ค์ด ๊ฐ๋
์ฑ ๋๋ฌธ์ ์์ฑ์ ํด์ค๋ค.
private final PostRepository postRepository;
@Transactional // (์์
์ ๋จ์) // ์ฐ๊ธฐ ์ง์ฐ ์ฒ๋ฆฌ๊น์ง
public Article save(ArticleDTO dto) {
// ๋น์ฆ๋์ค ๋ก์ง์ด ํ์ํ๋ค๋ฉด ์์ฑ...
return postRepository.save(dto.toEntity());
}
// ์ ์ฒด ๊ฒ์๊ธ ์กฐํ ๊ธฐ๋ฅ
public List<Article> findAll(){
List<Article> articles = postRepository.findAll();
return articles;
}
// ์์ธ ๋ณด๊ธฐ ๊ฒ์๊ธ ์กฐํ
public Article findById(Integer id) {
// Optional<T>๋ Java 8์์ ๋์
๋ ํด๋์ค์ด๋ฉฐ,
// ๊ฐ์ด ์กด์ฌํ ์๋ ์๊ณ ์์ ์๋ ์๋ ์ํฉ์ ๋ช
ํํ๊ฒ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ฌ์ฉ๋ฉ๋๋ค.
return postRepository.findById(id).orElseThrow(() -> new Exception400("ํด๋น ๊ฒ์๊ธ์ด ์์ต๋๋ค."));
}
@Transactional
public Article update(Integer id, ArticleDTO dto) {
// ์์ ๋ก์ง
Article articleEntity = postRepository
.findById(id).orElseThrow( () -> new Exception400("not found : " + id));
// ๊ฐ์ฒด ์ํ ๊ฐ ๋ณ๊ฒฝ
articleEntity.update(dto.getTitle(), dto.getContent());
// ์์์ฑ ์ปจํ
์คํธ -
// DB ์ save ์ฒ๋ฆฌ
//postRepository.save(articleEntity);
return articleEntity;
}
}
ํธ๋์ญ์ ์ฌ์ฉ์ ์ผ๋ฐ์ ์ธ ๊ท์น์ ์๋น์ค ๋ฉ์๋๊ฐ ์ฌ๋ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์ ํฌํจํ๊ฑฐ๋, ์์์ฑ ์ปจํ ์คํธ๋ฅผ ํตํด ์ํฐํฐ ๋ณ๊ฒฝ ์ฌํญ์ ์ถ์ ํด์ผ ํ๋ ๊ฒฝ์ฐ @Transactional์ ์ฌ์ฉํ์ฌ ํด๋น์ ์ํ ํ๋ค.
๐ก
ํธ๋์ญ์ ๊ณผ ์์์ฑ ์ปจํ ์คํธ์ ๊ด๊ณ
โ ํธ๋์ญ์ ์ด ์์๋๋ฉด ์์์ฑ ์ปจํ ์คํธ๋ ํ์ฑํ๋๋ค.
โ ํธ๋์ญ์ ๋ด์์ ์กฐํ๋ ์ํฐํฐ๋ ์์์ฑ ์ปจํ ์คํธ์์ ๊ด๋ฆฌ๋๋ ์์ ์ํ๊ฐ ๋๋ค.
๋ํฐ ์ฒดํน์ ๋ฉ์ปค๋์ฆ:
โ ์ํฐํฐ์ ํ๋ ๊ฐ์ ๋ณ๊ฒฝํ๋ฉด ์์์ฑ ์ปจํ ์คํธ๊ฐ ์ด๋ฅผ ๊ฐ์งํฉ๋๋ค.
โ ๋ณ๊ฒฝ๋ ์ํฐํฐ๋ ํธ๋์ญ์ ์ปค๋ฐ ์ DB์ ์๋์ผ๋ก ๋ฐ์๋ฉ๋๋ค.
save() ๋ฉ์๋์ ํ์์ฑ:
โ ์์ ์ํ์ ์ํฐํฐ๋ save()๋ฅผ ํธ์ถํ์ง ์์๋ ๋ณ๊ฒฝ ์ฌํญ์ด DB์ ๋ฐ์๋ฉ๋๋ค.
โ ์ค์์ ์ํ(detached)์ ์ํฐํฐ๋ ํธ๋์ญ์ ์ด ์๋ ๊ฒฝ์ฐ์๋ save()๋ฅผ ์ฌ์ฉํ์ฌ ๋ณ๊ฒฝ ์ฌํญ์ ์ ์ฅํด์ผ ํฉ๋๋ค.
์ฝ๋์ ํจ์จ์ฑ
โ ๋ถํ์ํ save() ํธ์ถ์ ์ค์
์ฃผ์ ๋ด์ฉ ์ ๋ฆฌ
BlogApiController ์ฝ๋ ์ถ๊ฐ
@RequiredArgsConstructor
@RestController // @controller + @responsebody
public class BlogApiController {
private final BlogService blogService;
// URL , ์ฆ, ์ฃผ์ ์ค๊ณ - http://localhost:8080/api/article
@PostMapping("/api/articles")
public ResponseEntity<Article> addArticle(@RequestBody ArticleDTO dto) {
// 1. ์ธ์ฆ ๊ฒ์ฌ
// 2. ์ ํจ์ฑ ๊ฒ์ฌ
Article savedArtilce = blogService.save(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(savedArtilce);
}
// URL , ์ฆ, ์ฃผ์ ์ค๊ณ - http://localhost:8080/api/articles
@GetMapping(value = "/api/articles", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiUtil<?> getAllArticles() {
List<Article> articles = blogService.findAll();
if(articles.isEmpty()) {
// return new ApiUtil<>(new Exception400("๊ฒ์๊ธ์ด ์์ต๋๋ค."));
throw new Exception400("๊ฒ์๊ธ์ด ์์ต๋๋ค.");
}
return new ApiUtil<>(articles);
}
// URL , ์ฆ, ์ฃผ์ ์ค๊ณ - http://localhost:8080/api/articles/1
@GetMapping(value = "/api/articles/{id}")
public ApiUtil<?> findArtilcle(@PathVariable(name = "id") Integer id) {
// 1. ์ ํจ์ฑ ๊ฒ์ฌ ์๋ต
Article article = blogService.findById(id);
return new ApiUtil<>(article);
}
// URL , ์ฆ, ์ฃผ์ ์ค๊ณ - http://localhost:8080/api/articles/1
@PutMapping(value = "/api/articles/{id}")
public ApiUtil<?> updateArticle(@PathVariable(name = "id") Integer id, @RequestBody ArticleDTO dto) {
// 1. ์ธ์ฆ ๊ฒ์ฌ
// 2. ์ ํจ์ฑ ๊ฒ์ฌ
Article updateArticle = blogService.update(id, dto);
return new ApiUtil<>(updateArticle);
}
}
์ถ๊ฐ ์ฝ์ด ๋ณด๊ธฐ
๐ก
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ HTTP ์์ฒญ์์ ์ ๋ฌ๋ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ ์ธก์ ์๋ฐ ๊ฐ์ฒด๋ ๋ฉ์๋ ํ๋ผ๋ฏธํฐ์ ์๋์ผ๋ก ๋ณํํ๊ณ ํ ๋นํ๋ ๊ณผ์ ์ ๋งํฉ๋๋ค. ์ด๋ฅผ ํตํด ๊ฐ๋ฐ์๋ ๋ณต์กํ ๋ฐ์ดํฐ ์ถ์ถ ๋ฐ ๋ณํ ๋ก์ง์ ์ง์ ๊ตฌํํ์ง ์๊ณ ๋ ๊ฐํธํ๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ฐธ๊ณ ์ฌํญ
- DispatcherServlet:
- Spring MVC์ ํ๋ก ํธ ์ปจํธ๋กค๋ฌ(Front Controller) ์ญํ ์ ํฉ๋๋ค.
- ๋ชจ๋ HTTP ์์ฒญ์ ๋ฐ์ ์ ์ ํ ์ปจํธ๋กค๋ฌ(Controller)๋ก ์ ๋ฌํฉ๋๋ค.
- ์์ฒญ ์ฒ๋ฆฌ ๊ณผ์ ์ ์ค์ ํ๋ธ๋ก, ์์ฒญ์ ๋ผ์ฐํ ๋ฐ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ ์กฐ์จํฉ๋๋ค.
- HandlerMapping:
- ์์ฒญ URL๊ณผ HTTP ๋ฉ์๋์ ๋ฐ๋ผ ์ ์ ํ ์ปจํธ๋กค๋ฌ ๋ฉ์๋๋ฅผ ๋งคํํฉ๋๋ค.
- ์๋ฅผ ๋ค์ด, @PutMapping("/api/articles/{id}")์ ๊ฐ์ ๋งคํ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ํด๋น ์์ฒญ์ ์ฒ๋ฆฌํ ๋ฉ์๋๋ฅผ ์ฐพ์ต๋๋ค.
- HandlerAdapter:
- ๋งคํ๋ ์ปจํธ๋กค๋ฌ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ , ํ์ํ ์ธ์๋ฅผ ์ ๊ณตํ๋ ์ญํ ์ ํฉ๋๋ค.
- HandlerMethodArgumentResolver๋ฅผ ์ฌ์ฉํ์ฌ ๋ฉ์๋ ํ๋ผ๋ฏธํฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉํฉ๋๋ค.
- HandlerMethodArgumentResolver:
- ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉํ๊ธฐ ์ํ ์ ๋ต์ ์ ์ํฉ๋๋ค.
- ๋ํ์ ์ธ ๊ตฌํ์ฒด๋ก๋ RequestParamMethodArgumentResolver, PathVariableMethodArgumentResolver, RequestBodyMethodArgumentResolver ๋ฑ์ด ์์ต๋๋ค.
- HttpMessageConverter:
- HTTP ์์ฒญ์ ๋ฐ๋์ ๋ด๊ธด ๋ฐ์ดํฐ๋ฅผ ์๋ฐ ๊ฐ์ฒด๋ก ๋ณํํ๊ฑฐ๋, ์๋ฐ ๊ฐ์ฒด๋ฅผ HTTP ์๋ต์ ๋ฐ๋๋ก ๋ณํํ๋ ์ญํ ์ ํฉ๋๋ค.
- Jackson ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ JSON ๋ฐ์ดํฐ๋ฅผ ์๋ฐ ๊ฐ์ฒด๋ก ๋ณํํ๋ MappingJackson2HttpMessageConverter๊ฐ ๋ํ์ ์ ๋๋ค.