끊임없이 검증하라

나에게 당연할지라도

Project

P1_클론 프로젝트(feat. 스프링부트와 AWS로 혼자 구현하는 웹 서비스)_3-2

fadet 2022. 3. 26. 23:20

 

❗이 포스트는 전 배달의민족, 현재 인프런에 계시고 유튜브 개발바닥의 크리에이터이신 개발자 이동욱님의 '스프링부트와 AWS로 혼자 구현하는 웹 서비스'를 기반으로 작성되었음을 알립니다. 포스트 맨 아래에 관련 링크가 있습니다. 책의 내용을 기반으로 작성되기에 실습 중이라면 책을 main 해당 포스트를 sub로 참고해주세요. 책의 설명이 부족한 부분 위주로 포스트가 구성됩니다.

❗ 이전 글에서 이어집니다! 따라서 잘 이해가 안되신다면 이전 글을 읽고 와주세요!

* 잘 모르시는 기술은 로그인 필요 없이 이 곳에서 AI에게 물어보세요!   

 


이전 포스트에선 entity와 Repository까지 다뤘었습니다. 이번 포스트는 3장의 나머지 부분에 대해 작성합니다.

 

3.4 등록/수정/조회 Api 만들기

 

# 스프링 웹 계층과 mvc 모델

지금부터 다룰 내용은 빌드 과정 중 가장 중요하다고 생각하는 API와 웹 계층 파트입니다. 그런만큼 책에서 간단히 소개한 내용에 대한 추가 설명을 하고 넘어가겠습니다. 웹 계층에 대한 이해는 스프링으로 도메인을 설계하는 과정 중 가장 핵심인 부분이라고 생각합니다. 아래 이미지는 책에 수록된 인포그래픽입니다.

각 layer에 대한 간단한 설명은 책에 있으니 생략하고 전체적인 흐름을 짚어보겠습니다. 스프링 mvc의 기본적인 구조 설명은 다른 블로그에 엄청나게 많으니 이 포스트에선 자세한 과정 대신 이해를 위해 그 흐름을 좀 쉽게 단순화해보겠습니다. 

 

[Client > DB direction] 1 dispatcher servlet(쉽게 컨트롤러의 컨트롤러라고 생각하면 편합니다. 이에 대한 자세한 설명은 나중에 추가 포스트를 작성하겠습니다)을 통해 요청에 맞는 handler(controller)를 찾아 매핑 -   [transaction begin]  2 controller가 요청에 맞는 service 호출 -  3 service가 repository 호출 - 4 repository는 DB에 요청에 맞는 CRUD 쿼리 전달

[DB > Client direction] 5 repository에서 service가 요청한 CRUD 쿼리에 대한 값 반환 - 6 service는 controller에게 비즈니스 로직에 대한 결과 반환  [transaction commit] - 7 controller는 service에게 받은 결과를 model에 담아 dispatcher servlet에게 전달 8 dispatcher servlet은 받은 결과를 바탕으로 요청에 맞는 view를 검색하여 moel을 전달 9 전달받은 model을 바탕으로 view는 client에게 rendering

 

이런 흐름으로 진행되고 이 과정에서 나온 dispatcher servlet이나 위 흐름에서 생략된 View resolver나 ModelAndView 등에 대한 내용은 책에 언급이 없으니 추후 포스트에서 mvc 내용과 함께 더 자세히 다루겠습니다. 위 흐름을 책에 나온 내용만 가지고 더 간단히 요약하자면 1 controller가 요청을 받아 service 호출 2 service는 repository 호출해서 비즈니스 로직 처리 3 비즈니스 로직 처리 결과를 controller가 받아 model에 담아 view로 전달 4 view에 rendering 이 될 것입니다.

 

위에서 살펴본 흐름은 스프링 mvc에 대한 내용이고 이 중 책에서 짚고 넘어가는 부분을 좀 더 소개하겠습니다.

 

1 위에서 살펴본 흐름 중 service가 포함된 과정 3, 6을 책에서는 트랜잭션 스크립트도메인 모델 두가지 방식으로 다룹니다. 트랜잭션 스크립트는 controller에서 비즈니스 로직에 대한 요청이 올때 모든 로직을 service 내에서 코드를 짜 처리하는 기존의 방식이고 service가 다루는 객체는 단순히 service의 메소드를 처리하기 위한 데이터 덩어리로 소개됩니다. 하지만 도메인 모델 방식은 이미 repository에 비즈니스 로직 처리에 대한 메소드가 다 존재하며 service는 이를 순서에 맞게 연결해주는 중간자로 존재하기에 트랜잭션과 도메인 간 순서만 보장해 준다고 소개됩니다.(사실 DDD의 설계 원칙상 비즈니스 로직은 도메인 엔티티 내에 존재하는게 더 맞을 수도 있습니다만 우선 넘어가겠습니다)

 

2 단순한 level에서 스프링 mvc의 과정을 진행할때 layer사이에 각 클래스들은 직접 서로에게 접근해도 setter를 사용하거나 하지 않는 이상 큰 문제는 없습니다. 하지만 서비스가 커지다보면 계층간 데이터 교환시 큰 어려움을 직면합니다. 책에서 DB와 View layer를 분리하는 것의 중요성은 충분히 설명했으니 좀 더 개발자 입장에서 와닿는 부분을 언급하겠습니다. 단적인 예로 client의 조회 요청에서 필요한 값은 post의 title과 created date일 때 만약 그대로 진행한다면 매번 코드가 바뀔때마다 개발자는 일일이 두 정보만 건드릴 것이라고 코드에 명시해야하고 modified date가 추가로 요구되면 일일이 관련 로직이 있는 코드를 다 고쳐야할 것입니다. 이를 보완하기 위해 로직 처리에 관여하지 않고 데이터 교환만을 위한 객체를 Dto입니다. 앞선 포스트에서 언급했듯 이 책에선 view controller와 api controller를 분리하고 api controller가 dto 객체를 적극적으로 사용하는데 api 요청은 주로 도메인 전체가 아닌 일부만 다루므로 그렇다고 생각하시면 되겠네요.

 

# PostsApiController

이제 빠르게 책 내용을 짚고 넘어가겠습니다. 사용자로부터 post에 관한 api 요청이 오면 전부 처리해줄 PostApiController를 작성합니다. 여기서 살펴볼 것은 단순한 view controller에선 model에 값을 담아 view로 전달 2 요청에 맞는 view page를 server에서 매핑이지만 api controller에선 dto 객체를 통해 service를 호출합니다.

// View Controller
public class IndexController {
...
    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute("posts", postsService.findAllDesc());
        return "index";
--------------------------------------------------------------------------------        
// Api Controller
public class PostsApiController {
...
     @PostMapping("/api/v1/posts")
     public Long save(@RequestBody PostsSaveRequestDto requestDto) {
     	return postsService.save(requestDto);

# PostsService

다음은 PostsService입니다. 여기에서 짚고 갈 내용은 service의 각 메소드에는 모두 @Transactional이 붙어야만 합니다. 트랜잭션에 대해서는 알거라 생각하지만 간단히 작업의 시작과 끝을 한 단위로 묶어 그 사이에 다른 issue가 발생할 여지를 없애는 것이라 알고 넘어가시면됩니다. 예시를 들자면 은행 앱에서 송금을 한 transaction으로 처리하지 않으면 중간에 송금 과정에서 문제가 발생했을때 출금계좌에선 돈이 빠져나갔는데 입금계좌는 변동이 없는, 한 마디로 돈이 증발하는 일(사실 계좌에 찍힌건 돈이 아닌 숫자긴 합니다만)이 발생할 수 있고 이는 매우 critical한 문제입니다.

 

또 하나 더 있습니다. 사실 스프링에서 가장 중요한 DI(dependency injection)을 알고 계시는 분들에겐 큰 문제는 없겠습니다만 @Autowired가 생략된 과정을 의아하게 여기실 수 있어 아래 코드를 첨부합니다.

@RequiredArgsConstructor // 1 - 해당 어노테이션으로 생성자 코드 대체 
@Service
public class PostsService {
    // 3 - @Autowired를 통한 주입 대신 생성자 주입이 가능
    // 2 - private final이 붙어 @Reqired...가 해당 필드를 인자로 생성자 추가
    private final PostsRepository postsRepository; 
...
    // 모든 메소드는 @Transactional 필수
    @Transactional
    public Long save(PostsSaveRequestDto requestDto) {
        return postsRepository.save(requestDto.toEntity()).getId();
    }

앞서 다뤘듯 롬복과 활용하여 생성자를 굳이 내가 쓸 필요도 없고 주입받을 객체만 선언해주면 됩니다. @Autowired를 생략할 수 있는 이유는 위와 같지만 근본적으로는 스프링에서 생성자 주입을 매우 적극적으로 권장하고 있기에 가능합니다. 생성자 주입이 가장 권장되는 이유는 책에도 언급되어 있지만 수정자나 필드주입을 사용하면 이후 수정의 여지가 생겨 OOP의 OCP(개방 폐쇄 원칙)를 위반하는 것이 가장 큰 이유입니다.(책에 나온 이유 말고도 불변성 보장, 순환 참조 감지도 중요합니다만 우선 넘어가겠습니다.) 만약 DI를 모르시면 DI는 가볍게 설명하고 넘어갈 부분이 아니라고 생각하기에 다른 포스트를 보고 개략적인 내용이라도 공부하시고 책을 진행하시길 바랍니다.

 

# PostsSaveRequestDto

PostsSaveRequestDto는 말그래도 post를 생성하는 요청이 오면 DB에 저장하기 위해 사용하는 dto 클래스입니다. 이 부분에서 알아가야 할 것은 dto가 entity를 받을지 말지의 판단 정도입니다. 이건 생각해보면 당연합니다. client에게 오는 정보를 entity로 변환해줘야하는(심지어 toentity 메소드가 있는...) requestDto와는 달리 responseDto는 생성자를 만들 때 entity를 받아 사용해도 무방합니다.

public class PostsSaveRequestDto {
...
  public Posts toEntity() { // to의 의미를 알면...
        return Posts.builder()
                .title(title)
                .content(content)
                .author(author)
------------------------------------------------------------------------------             
public class PostsResponseDto {
...
  public PostsResponseDto(Posts entity) { // param이 entity
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();

# PostsApiControllerTest

그 다음에 작성하는 PostsApiControllerTest에서 사용되는 restTemplate은 가볍게 HTTP 요청을 보내는 클라이언트 도구이며 개발자가 일일이 코드를 작성해야하는 기계적인 부분을 줄여주는 라이브러리라 생각하고 넘어가시면 됩니다. 어차피 이후 과정에서 삭제되는 코드입니다. 그래도 간단히 설명하자면 requestDto를 인자로 받아 responseEntity를 만들고 restTemplate에 이 데이터와 get,post,put 등의 http 요청 방식, 위의 port가 포함된 url을 인자로 전달해주면 json이 반환됩니다.

 

# PostsResponseDto

PostsResponseDto는 client에게 반환되는 데이터를 담는 dto 클래스입니다. 앞에서 언급했었지만 dto 생성자를 보면 entity를 받아서 처리하는데 이에 익숙하지 않으신 분은 '아까 View와 DB layer를 분리하라 했으니 dto와 entity를 분리해야하지 않나?'라는 의문을 가지실 수 있을텐데, Domain 클래스에 setter는 사용하면 안되지만 getter는 사용하는 것과 비슷한 이치로 dto는 entity를 수정할 수 없고 참조만 하는거니 괜찮습니다. 이런 의문이 드실땐 의존성의 주체와 객체에 대해서 다시 한 번 생각하시길 바랍니다. dto는 entity를 의존해도되지만 그 반대는 절대 안됩니다.

 

# PostsService

다음 부분들은 쉬우니 넘어가고 PostsService를 살펴보겠습니다. JPA에서 업데이트 부분을 공부할땐 merge와 준영속에 대한 파트를 공부하게 되지만 책은 이 내용을 다루지 않고 단순히 영속성 컨텍스트가 유지되어 update 쿼리를 날리지 않는다고만 소개되어 있습니다. 따라서 해당 부분이 잘 이해가 되지 않으시면 다른 포스트들을 통해 공부해보시는 것도 나쁘지 않습니다만 책을 빠르게 완독하실 분은 책의 내용만 알고 넘어가셔도 후에 문제가 되지는 않습니다.

 

# PostsControllerTest

이후 진행하는 테스트가 아닌 직접 조회 기능을 확인하는 과정은 이 포스트대로 h2 DB를 설정했다면 책 내용대로 진행하지 않아도 작동합니다.

 

3.5 JPA Auditing으로 시간 설정 자동화

 

# JPA Auditing

해당 내용은 책 내용을 진행함에 있어 큰 어려움은 없을 것입니다. 전체적인 과정은 Auditing 관련 time 클래스를 작성하고 그 클래스를 주요 entity가 상속하게끔하면 끝입니다. 처음 주요 entity를 작성할 때 작성, 수정 시간 관련 필드는 매우 중요한 column이 되며 이에 대한 로직도 생각해야하지만 이런 과정을 JPA Auditing기능을 사용함으로써 시간을 아낄 수 있습니다.


이로써 제일 중요하다고 생각하는 3장을 마쳤습니다. 하지만 중요성과 난이도, 학습 시간이 늘 비례하진 않습니다. 4장은 화면을 만들기 시작하는데 꽤나 과정이 길기에 포스트를 나눌 것 같네요. 아 그리고 3장은 전체적인 구조만 알고있다면 난이도가 그리 높지않아 코드블럭을 쓴 내용이 별로 없기에 책 내용 그대로 진행하셔도 큰 문제가 없습니다.

 

refer

이동욱님 블로그의 관련 포스트 : https://jojoldu.tistory.com/539?category=717427

개발바닥 유튜브 :https://www.youtube.com/channel/UCSEOUzkGNCT_29EU_vnBYjg

 

개발바닥

본격 세계최초 DEV 엔터테인먼트 토크쇼 두 스타트업 개발자의 요절복통 이야기 구독 안하면 장애남!!

www.youtube.com

이동욱님 github의 해당 repository : https://github.com/jojoldu/freelec-springboot2-webservice