Spring Boot, CRUD API 개발을 위한 기록 -2- (MVC 아키텍처 구성과 API)

Spring Boot의 기본적인 MVC 아키텍쳐와 흐름에 대해 알아보고 간단한 API 를 구현 해보기로 한다.

Spring Boot, CRUD API 개발을 위한 기록 -2- (MVC 아키텍처 구성과 API)
Photo by Kaleidico / Unsplash

Spring Boot 기반으로 MVC, CRUD API 를 개발하기 위한 기록을 남기도록 한다. 일부 개념에 대한 설명은 GPT-4 를 이용해서 작성했다.

본격적인 프로젝트를 시작하면서 Spring Boot의 기본적인 MVC 아키텍쳐와 흐름에 대해 알아보고 간단한 API 를 하나 구현 해보기로 한다.

1. 아키텍처 확인

기본적인 API 를 호출 했을때 요청에서 응답까지 구성할 Spring Boot 내부의 흐름을 따라가 보기로 한다.

Spring Boot, MVC Flow Architecture
1) Dispatcher Servlet

모든 클라이언트의 요청을 가장 먼저 받아서 적절한 컨트롤러나 핸들러에게 위임하고, 그 결과를 받아서 형식에 맞게 응답하는 역할을 한다.

2) Handler Mapping

Dispatcher Servlet은 요청을 처리할 핸들러를 찾고 해당 객체의 메소드를 호출한다. 따라서 가장 먼저 어느 컨트롤러가 요청을 처리할 수 있는지를 식별해야 하는데, 해당 역할을 하는 것이 바로 Handler Mapping 이다.

3) Handler Adapter

실제 로직을 구현한 핸들러를 실행하고 처리하는 역할을 하거나 요청의 처리 결과를 적절한 응답으로 변환하는 역할을 수행한다.

4) Handler (Controller)

컨트롤러는 요청을 받아서 적절한 서비스를 호출하고 서비스로부터 받은 결과를 HTTP 응답으로 변환하여 전송한다.

5) DTO (Data Transfer Object)

단순히 데이터를 한 시스템에서 다른 시스템으로 전달하는 역할을 하는 객체로서 당연히 비즈니스 로직은 가지고 있지 않다.

6) Service

비즈니스 로직을 구현하는 계층이며 서비스는 컨트롤러로부터 요청을 받아서 필요한 처리를 수행하고 그 결과를 컨트롤러에게 반환한다.

7) Repository

리포지토리는 서비스로부터 데이터 접근에 관한 요청을 받아서 데이터베이스와 통신하고, 그 결과를 서비스에게 반환한다.

8) DAO (Data Access Object)

DAO는 JDBC를 통해 특정 쿼리문을 실행하거나 결과를 받아 오는 역할을 한다.

9) Entity

데이터베이스의 테이블에 존재하는 컬럼을 속성으로 가지고 있고 비즈니스 로직을 가지고 있다.

2. 디렉토리 구성

src
└── main
    └── java
        └── com.example.sample
         	├── controller
         	├── dao
          	├── dto
        	├── entity
         	├── repository
         	└── service

패키지 내 디렉토리 구성

3. Member, POST API 만들기

디렉토리 구조와 아키텍쳐에 맞는 기능들을 구성하며 사용자를 추가하는 API 를 만들어 보고자 한다.

1) Entity 작성

Entity 는 데이터베이스의 테이블을 나타내는 자바 클래스를 의미하며 다음과 같은 특징을 가지고 있음.

  • @Entity 어노테이션
    : JPA가 해당 클래스를 엔티티로 인식하게 하기 위해, 클래스 선언 위에 @Entity 어노테이션이 붙어야 한다.
  • 식별자 필드
    : 각 엔티티 인스턴스를 유일하게 식별할 수 있는 식별자 필드가 필요한데 @Id 어노테이션으로 지정한다.
  • @Table 어노테이션
    : 데이터베이스의 테이블 이름과 엔티티 이름이 다른 경우, @Table 어노테이션으로 테이블 이름을 지정할 수 있다.
  • @Column 어노테이션
    : 테이블의 컬럼(column)과 엔티티의 속성(property)이 다른 경우, @Column 어노테이션으로 컬럼 이름을 지정할 수 있으며 컬럼의 길이, 널(null) 허용 여부, 유니크(unique) 여부 등도 지정할 수 있다.

아래에서는 Member 에 대한 Entity 를 작성 해보았다.

package com.example.sample.entity;

import jakarta.persistence.*;
import lombok.*;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "member")
@Builder
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "mem_no")
    private Long No;

    @Column(name = "id", unique = true, nullable = false, length = 8)
    private String Id;

    @Column(name = "name", nullable = false, length = 24)
    private String Name;

    @Column(name = "age")
    private Integer Age;

    @Enumerated(EnumType.STRING)
    private Gender Gender;

    @Column(name = "level", nullable = false)
    private Integer Level;
    
}

entity/Member.java

package com.example.sample.entity;

public enum Gender {
    MALE,
    FEMALE
}

entity/Gender.java

CREATE TABLE `member` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(128) NOT NULL,
  `age` int(2) NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

Member Table Schema

2) DTO 작성

DTO(Data Transfer Object)란 계층간 데이터 교환을 위해 사용하는 객체이며 아래에서는 Member 에 대한 DTO 를 Inner Class 로 작성 해보았다.

package com.example.sample.dto;

import com.example.sample.entity.Gender;
import com.example.sample.entity.Member;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

public class MemberDTO {

    @Getter
    @Builder
    @ToString
    public static class Create {

        @JsonProperty("id")
        private String Id;
        
        @JsonProperty("name")
        private String Name;
        
        @JsonProperty("age")
        private Integer Age;
        
        @JsonProperty("gender")
        private Gender Gender;
        
        @JsonProperty("level")
        private Integer Level;

    }

}

dto/MemberDTO.java

3) DAO 작성

DAO(Data Access Object)란 데이터베이스와 연동하여 데이터를 저장하고 조회하는 역할을 하는 인터페이스이며 스프링의 DAO 지원 기능을 사용하여 JDBC, Hibernate, JPA, JDO 등의 다양한 데이터 접근 기술을 일관된 방식으로 사용할 수 있다.

아래는 Member 에 대한 Repository와 DAO 를 작성 해보았다.

package com.example.sample.repository;

import com.example.sample.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
}

repository/MemberRepository.java

package com.example.sample.dao;

import com.example.sample.entity.Member;
import com.example.sample.repository.MemberRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class MemberDAO {
    private final MemberRepository repository;

    @Autowired
    public MemberDAO(MemberRepository repository) {
        this.repository = repository;
    }

    public Member Create(Member entity) {

        log.warn(entity.toString());

        return this.repository.save(entity);
    }
}

dao/MemberDAO.java

4) Service 작성

Service 란 비즈니스 로직을 수행하는 클래스이다. 비즈니스 로직이란 웹 애플리케이션의 핵심적인 기능을 구현하는 코드를 말하며 예를 들어, 회원 가입, 로그인, 주문, 결제 등의 기능들이 비즈니스 로직에 해당한다.

Service 클래스는 @Service 어노테이션을 붙여서 스프링 컨테이너에 빈으로 등록하고 Controller 클래스와 Repository 클래스와 협력하여 웹 요청을 처리하고 데이터베이스와의 작업을 수행한다.

package com.example.sample.service;

import com.example.sample.dao.MemberDAO;
import com.example.sample.dto.MemberDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MemberService {

    private final MemberDAO dao;

    @Autowired
    public MemberService(MemberDAO dao) {
        this.dao = dao;
    }

    public MemberDTO.Create Create(MemberDTO.Create dto) {
        return this.dao.Create(dto.ToEntity()).ToCreateDTO();
    }
}

service/MemberService.java

  • DTO 를 Entity 로 변환
    비즈니스 로직에서 DTO 오브젝트를 Entity 오브젝트로 변환하기 위해 MemberDTO 내 Create 클래스에 ToEntity 함수를 다음과 같이 추가 했다.
package com.example.sample.dto;

import com.example.sample.entity.Gender;
import com.example.sample.entity.Member;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

public class MemberDTO {

    @Getter
    @Builder
    @ToString
    public static class Create {

        @JsonProperty("id")
        private String Id;

        @JsonProperty("name")
        private String Name;

        @JsonProperty("age")
        private Integer Age;

        @JsonProperty("gender")
        private Gender Gender;

        @JsonProperty("level")
        private Integer Level;

        public Member ToEntity() {
            return Member.builder()
                    .Id(this.Id)
                    .Name(this.Name)
                    .Age(this.Age)
                    .Gender(this.Gender)
                    .Level(this.Level)
                    .build();
        }

    }

}

dto/MemberDTO.java

  • Entity 를 DTO 로 변환
    비즈니스 로직에서 Entity 오브젝트를 Create DTO 오브젝트로 변환하기 위해 Member Entity 클래스에 ToCreateDTO 함수를 다음과 같이 추가 했다.
package com.example.sample.entity;

import com.example.sample.dto.MemberDTO;
import jakarta.persistence.*;
import lombok.*;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "member")
@Builder
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "mem_no")
    private Long No;

    @Column(name = "id", unique = true, nullable = false, length = 8)
    private String Id;

    @Column(name = "name", nullable = false, length = 24)
    private String Name;

    @Column(name = "age")
    private Integer Age;

    @Enumerated(EnumType.STRING)
    private Gender Gender;

    @Column(name = "level", nullable = false)
    private Integer Level;

    public MemberDTO.Create ToCreateDTO() {
        return MemberDTO.Create.builder()
                .Id(this.Id)
                .Name(this.Name)
                .Age(this.Age)
                .Gender(this.Gender)
                .Level(this.Level)
                .build();
    }

}

entity/Member.java

5) Controller 작성

Controller 클래스는 웹 요청과 응답을 담당하고, Service 클래스에게 비즈니스 로직을 위임한다.

아래는 Member 정보를 POST로 요청받는 간단한 로직을 추가 해봤다.

package com.example.sample.controller;

import com.example.sample.dto.MemberDTO;
import com.example.sample.service.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/api")
@Slf4j
public class MemberController {

    private final MemberService service;

    @Autowired
    public MemberController(MemberService service) {
        this.service = service;
    }

    @PostMapping("/member")
    public MemberDTO.Create PostMember(
            @RequestBody MemberDTO.Create dto
    ) {
        log.warn(dto.toString());
        return this.service.Create(dto);
    }
}

controller/MemberController.java

4. Spring Doc 확인

의존성에 springdoc-openapi-starter-webmvc-ui 을 주입 했다면 아래 주소에서 Swagger 문서를 확인 할 수 있다. 이번에는 실행확인만 하고 SpringDoc 에 대해서는 따로 정리 하기로 한다.

OpenAPI 3 Library for spring-boot
Library for OpenAPI 3 with spring boot projects. Is based on swagger-ui, to display the OpenAPI description.Generates automatically the OpenAPI file.
http://localhost:8080/swagger-ui/index.html
SpringDoc 에서 Post API 기능 확인
Database 에서 데이터 삽입 확인