[Spring Boot] 게시판 구현 2 - 게시물 등록/수정/조회/삭제 API 만들기 ①

joyful·2021년 4월 30일
7

Java/Spring

목록 보기
10/28

우선 API를 만들기 위해서는 총 3개의 클래스가 필요하다.

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

여기서 주의할 점은 Service는 비지니스 로직을 처리하지 않는다는 것이다. 즉, 트랜잭션, 도메인 간 순서 보장의 역할만 수행한다.
그렇다면 비지니스 로직은 어디에서 처리해야 할까?

🔎 Spring Web Layer

  1. Web Layer
    • 뷰 템플릿 영역
      ex) 컨트롤러(@Controller), JSP, Freemarker 등
    • 외부 요청응답에 대한 전반적인 영역
      ex) 필터(@Filter), 인터셉터, 컨트롤러 어드바이스(@ControllerAdvice) 등
  2. Service Layer
    • @Service에 사용되는 서비스 영역
    • 일반적으로 Controller와 DAO의 중간 영역에서 사용
    • @Transactional이 사용되어야 하는 영역
  3. Repository Layer
    • Database와 같이 데이터 저장소에 접근하는 영역
  4. DTOs
    • 계층 간에 데이터 교환을 위한 객체인 DTO(Data Transfer Object)의 영역
      ex) 뷰 템플릿 엔진에서 사용될 객체, Repository Layer에서 결과로 넘겨준 객체 등
  5. Domain Model
    • 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해하며 공유할 수 있도록 단순화시킨 것
      ex) 택시 앱의 배차, 탑승, 요금 등
    • @Entity가 사용된 영역
    • VO와 같은 값 객체가 해당되는 영역
      → 데이터베이스의 테이블과의 매핑이 필수는 아님

잠시 Spring 웹 계층에 대해 살펴보았다. 이 중 비지니스 처리를 담당하는 곳은 어디일까? 바로 Domain이다.
왜 Service에서 처리하지 않고 Domain에서 처리하는 것일까?

💡 도메인에서 비지니스 로직을 처리하는 이유

사실, 기존에는 Service가 비지니스 처리를 담당했었다. 이 방식을 트랜잭션 스크립트라고 하는데, 모든 로직을 서비스 클래스 내부에서 처리한다. 이 방식은 구현이 매우 쉽다는 것이 최대 장점이다. 그 이유는 구현 방법이 단순하기 때문이다.

그러나 이 방식은 비즈니스 로직이 복잡해지면 난잡한 코드를 만들게 된다. 애초에 도메인에 대한 분석 및 설계 개념이 약하기 때문에 코드의 중복 발생을 막기 어려워진다. 또한, 이 방식을 사용 시 쉬운 개발에 익숙해지므로, 공통된 코드를 공통 모듈로 분리하지 않고 복사&붙이기 방식으로 중복 코드를 만드는 유혹에 빠지기 쉽다.

반면에 도메인 모델객체 지향에 기반한 재사용성, 확장성, 그리고 편리한 유지 보수 등의 장점이 있다. 일단 도메인 모델을 구축하면 언제든지 재사용 가능하다. 또한, 상속/인터페이스, 더 나아가 컴포넌트 개념을 바탕으로 도메인 모델을 개발하게 되면 무한한 확장성을 갖게 된다. 또한 이런 점들은 빠른 개발에 일조한다.

이러한 이유 때문에 도메인에서 비즈니스 로직을 처리하는 방식을 택한 것이다.

궁금증도 해결했으니 이제 CRUD 기능을 만들어보자. 이번 시간에는 등록(Create)과 수정(Update)을 구현해볼 것이다.


📝 등록 API - Create

BoardController

@RequiredArgsConstructor
@RestController
public class BoardController {
    private final BoardService boardService;

    @PostMapping("/board")
    public Long create(@RequestBody BoardCreateRequestDto requestDto) {
        return boardService.create(requestDto);
    }

}

BoardService

@RequiredArgsConstructor
@Service
public class BoardService {
    private final BoardRepository boardRepository;

    @Transactional
    public Long create(BoardCreateRequestDto requestDto) {
        return boardRepository.save(requestDto.toEntity()).getId();
    }

Spring에서는 Bean을 주입받는 방식들이 다음과 같이 존재한다.

@Autowired     ◾ setter      ◾ 생성자

여기서 @Autowired는 사용법이 제일 간단하나 권장되지 않는다. 대신 가장 권장되는 방식은 생성자 주입인데, 이유는 다음과 같다.

  • 순환 참조 방지
  • 편리한 테스트 코드 작성
  • 코드의 품질 향상
  • 불변성 보장
  • 오류 사전 방지

그렇다면 생성자는 어떻게 생성할까?

바로 롬복의 @RequiredArgsConstructor로 해결할 수 있다. 이 어노테이션은 final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성해준다.

여기서 생성자 대신 롬복 어노테이션을 사용한 이유는 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함이다.

자, 이제 Controller와 Service에서 사용할 DTO 클래스를 생성해보자.

BoardCreateRequestDto

@Getter
@NoArgsConstructor
public class BoardCreateRequestDto {
    private Member member;
    private String title;
    private String content;

    @Builder
    public BoardCreateRequestDto(Member member, String title, String content) {
        this.member = member;
        this.title = title;
        this.content = content;
    }

    public Board toEntity() {
        return Board.builder()
                .member(member)
                .title(title)
                .content(content)
                .build();
    }
}

위의 코드를 보면 Entity 클래스와 거의 유사하다는 것을 알 수 있다. 그럼에도 불구하고 DTO 클래스를 추가로 생성한 이유는 다음과 같다.

Entity 클래스는 데이터베이스와 맞닿은 핵심 클래스이며, 이를 기준으로

  1. 테이블이 생성 및 스키마가 변경되고
  2. 수많은 서비스 클래스나 비즈니스 로직들이 동작한다.

이 때문에 Entity 클래스가 변경되면 여러 클래스에 영향을 끼치게 된다.
사소한 기능 변경들은 자주 일어나는데, 이를 위해 Entity 클래스를 변경하는 것은 문고리를 교체하기 위해 문 자체를 뜯어고치는 것과 같다.
반면에 Request와 Response용 DTO는 View를 위한 클래스로, 자주 변경되어도 데이터베이스 자체에 영향을 주지 않는다.

따라서 Entity 클래스와 Controller에서 쓸 DTO는 꼭 분리해서 사용해야 한다.


📝 수정 API - Update

BoardController

@RequiredArgsConstructor
@RestController
public class BoardController {

    ...

    @PutMapping("/board/{id}")
    public Long update(@PathVariable Long id, @RequestBody BoardUpdateRequestDto requestDto) {
        return boardService.update(id, requestDto);
    }

}

BoardUpdateRequestDto

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

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

Board

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

BoardService

@RequiredArgsConstructor
@Service
public class BoardService {

    ...

    @Transactional
    public Long update(Long id, BoardUpdateRequestDto requestDto) {
        Board board = boardRepository.findById(id)
        	.orElseThrow(() -> new
            IllegalArgumentException("해당 게시글이 존재하지 않습니다."));

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

        return id;
    }
    
    

위의 코드를 살펴보면 update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없다. 그럼에도 불구하고 update가 가능한 이유는 다음과 같다.

💡 더티 체킹(Dirty Checking)

  • JPA의 영속성 컨텍스트
    • 영속성 컨텍스트 = 엔티티영구 저장하는 환경
    • JPA의 엔티티 매니저(EntityManager)가 활성화된 상태로 트랜잭션 내에서 데이터베이스의 데이터를 가져옴 → 영속성 컨텍스트 유지 상태

영속성 컨텍스트가 유지된 상태에서 해당 데이터의 값을 변경할 경우, 트랜잭션이 종료되는 시점에 해당 테이블에 변경분을 반영한다.

그 결과 Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없는 것이다.




📖 참고

profile
기쁘게 코딩하고 싶은 백엔드 개발자

1개의 댓글

comment-user-thumbnail
2023년 11월 5일

@Builder 어노테이션 사용한 Board 클래스에서 Board.builder() 이렇게 불러왔을때 Non-static method 'builder()' cannot be referenced from a static context 오류뜨는데 어떻게 하신건가요

답글 달기