μΉ΄ν…Œκ³ λ¦¬ μ—†μŒ

[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κ°€ λŒ€ν‘œμ μž…λ‹ˆλ‹€.