Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

채채

3-4. 게시물 등록/수정/조회/삭제 API 만들기 본문

spring boot

3-4. 게시물 등록/수정/조회/삭제 API 만들기

HChaeEun 2022. 11. 8. 13:15

API를 만들기 위해선 3가지 클래스가 필요하다.

  • Dto: request 데이터 수신
  • Controller: API 요청 수신
  • Service: 트랜잭션, 도메인 기능 간 순서 보장

Service에서는 비즈니스 로직(규칙에따라 데이터를 생성, 표시, 저장, 변경)을 처리하지 않는다.

그러면 비즈니스 로직은 누가 처리할까?

 

➡️ Spring 웹 계층

Spring 웹 계층

1. Web Layer

  • 뷰 템플릿 영역

        ex) 컨트롤러(@Controller), JSP/Freemarker 등

  • 외부 요청과 응답

        ex) 필터(@Filter), 인터셉터, 컨트롤러 어드바이스(@ControllerAdvice) 등

2. Service Layer

  • @Service, @Transactional이 사용되는 영역
  • Controller와 Dao의 중간 영역

3. Repository Layer

  • 데이터 저장소에 접근하는 영역 like Dao(Data Access Object)

4. Dtos

  • Dto(Data Transfer Object)의 영역

      -> 계층 간에 데이터 교환을 위한 객체

      -> 뷰 템플릿 엔진에서 사용될 객체, Reopository Layer에서 결과로 넘겨준 객체

5. Domain Model

  • 도메인(개발 대상)을 동일한 관점으로 이해하기 쉽도록 단순화한 것 <택시 앱에서 배차, 탑승, 요금 등>
  • @Entity가 사용된 영역
  • 무조건 데이터베이스의 테이블과 관계가 있어야하는 것은 아니다.

        -> VO와 같은 값 객체도 이 영역에 해당

 

이 다섯가지 Layer에서 비지니스 처리를 담당해야할 곳은 Domain

 

❓왜 Service가 아닌 Domain영역에서 처리할까

기존에는 Domain이 아닌 Service로 비지니스 처리를 담당했다. 이러한 처리 방식을 트랜잭션 스크립트라고 한다.

이 방식은 구현 방법이 단순하다는 장점이 있으나, 모든 로직이 서비스 클래스 내부에서 처리되면 서비스 계층이 무의미해지며, 객체가 단순히 데이터 덩어리의 역할만 하게 된다.

 

반면에 도메인 모델에서 처리할 경우 객체 지향에 기반하므로 재사용성, 확장성이 높아지며 편리하게 유지 보수를 할 수 있다. 이러한 점들은 빠른 개발에 일조한다.

 

📜등록 API - save

PostApiController

// import 코드 생략

@RequiredArgsConstructor
@RestController
public class PostsApiController {
    private final PostsService postService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto) {
        return postService.save(requestDto);
    }
}

PostService

// import 코드 생략

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto) {
        return postsRepository.save(requestDto.toEntity()).getId();
    }
}

 

스프링에서  Bean(어플리케이션 객체)을 주입받는 방식은 다음과 같다.

https://ocblog.tistory.com/44

◾ @Autowired                ◾ setter                   ◾ 생성자

가장 권장하는 방식은 @RequiredArgsConstructor생성자를 생성하여 Bean을 주입받는 것이다.

(@Autowired는 사용방법이 제일 간단하나 권장하지 않음)

 

final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성해주는 어노테이션 @RequiredArgsConstructor을 활용하는 것이다. 생성자 대신 롬복 어노테이션을 사용한 이유는 해당 클래스의 의존성 관계가 변경될 때마다, 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함이다.

 

다음으로는 Controller와 Service에서 사용할 Dto 클래스이다.

 

PostsSaveRequestDto

// import 코드 생략

@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
    private String title;
    private String content;
    private String author;
    
    @Builder
    public PostsSaveRequestDto(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }

    public Posts toEntity() {
        return Posts.builder()
                .title(title)
                .content(content)
                .author(author)
                .build();
    }
}

위의 코드는 Entity 클래스와 거의 유사하지만, Dto 클래스를 추가로 생성했다.

이는 Entity 클래스가 데이터 베이스와 맞닿은 핵심 클래스이기 때문이다. 화면을 변경하는 것은 사소한 기능 변경인데, 이를 위해 테이블과 연결된 Entity 클래스를 변경하는 것은 너무 큰 변경이기 때문이다.

즉 요약하자면, 

  • Entity 클래스가 변경되면 여러 클래스가 영향을 받는다.
  • RequestResponseDtoView를 위한 클래스이기에 자주 변경해야한다.
  • 따라서 View Layer와 DB Layer의 역할 분리를 위해 Dto 클래스를 추가로 생성한다.

📜수정 API - update

PostsApiController

@RequiredArgsConstructor
@RestController
public class PostsApiController {
    
    ...

    @PutMapping("api/v1/posts/{id}")
    public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto) 
    {
        return postService.update(id, requestDto);
    }
}

PostUpdateRequestDto

@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
    private String title;
    private String content;

    @Builder
    public PostsUpdateRequestDto(String title, String content)
    {
        this.title = title;
        this.content = content;
    }
}

Posts

public class Posts {
	...
    
    public void update(String title, String content) 
    {
        this.title = title;
        this.content = content;
    }
}

PostService

@RequiredArgsConstructor
@Service
public class PostsService {
	...
    
    @Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto) {
        Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id="+ id));

        posts.update(requestDto.getTitle(), requestDto.getContent());

        return id;
    }
}

위의 코드를 살펴보면 update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없다. 이게 가능한 이유는 JPA의 영속성 컨텍스트 때문이다.

 

➡️더티 체킹(dirty checking)

🔹JPA의 영속성 컨텍스트란?
      - 엔터티를 영구 저장하는 환경
      - JPA의 엔티티 매니저(EntityManager)가 활성화된 상태로 트랜젝션 내에서 데이터베이스의 데이터를 가져오면, 이           데이터는 영속성 컨텍스트가 유지된 상태

영속성 컨텍스트가 유지된 상태에서 해당 데이터의 값을 변경하면, 트랜젝션이 끝나는 시점에 해당 테이블에 변경분을 반영함.

따라서 Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없다.

 

📜조회 API - response

PostsApiController

@RequiredArgsConstructor
@RestController
public class PostsApiController {
	private final PostsService postService;
    
    ...
    
    @GetMapping("api/v1/posts/{id}")
    public PostsResponseDto findById (@PathVariable Long id) {
        return postService.findById(id);
    }
}

PostsService

@RequiredArgsConstructor
@Service
public class PostsService {
   ...

    public PostsResponseDto findById (Long id) {
        Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));

        return new PostsResponseDto(entity);
    }
}