μΉ΄ν
κ³ λ¦¬ μμ
[JPA] κΈ μμΈλ³΄κΈ°(μ‘°ν) API ꡬν - 7
λ―Έλ‘910
2024. 10. 2. 17:30
π‘
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κ° λνμ μ λλ€.