[Spring Boot] Spring Boot로 HTTP API 설계하기
계층 설계
서버에 모든 로직이 한 class안에 들어있으면, 개발이 진행됨에 따라 class 안에 자연스럽게 code의 양이 많아진다.
따라서 한 객체에게 많은 책임이 부여되기 때문에, 객체지향의 원칙에서 벗어나게 된다.
견고한 서버 설계를 위해서 보통 계층을 나눠서 서버를 설계한다. 아래 예제에서 Controller, Service, Domain, Repository 계층으로 나눠서 설계했다.
JPA (Java Persistence Api)
- JPA는 Java 표준 ORM이다.
- Node.js의 경우 sequelize, typeorm, prisma 등 다양한 ORM이 있었는데, Spring의 경우 JPA라는 표준 ORM이 있어서 개발하기에 훨씬 수월하다고 생각한다.
JPA 설치
build.gradle
에 아래 dependency를 추가해준다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
Database 설정
- h2 database를 사용하기 위해서
build.gradle
에 h2 database를 명시해준다. - H2 Database는 Spring Boot에서 사용할 수 있는 Java 기반의 In-Memory Database이다.
dependencies {
runtimeOnly 'com.h2database:h2'
}
설정 파일
설정 파일의 경우, spring initializer로 Spring Boot project를 시작하면, 기본적으로 application.properties가 들어있지만, yaml 파일의 경우 훨씬 가독성 좋게 script를 작성할 수 있기 때문에 application.yaml 파일을 만들어 script를 작성했다.
# Server port 설정
server:
port: 1234
# datasource 명시
datasource:
url: jdbc:h2:mem:testDb
username: sa
password:
# h2 database driver
driver-class-name: org.h2.Driver
## JPA 설정
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
logging.level:
org.hibernate.SQL: debug
ddl-auto
- table 생성 방법에 대한 정의를 할 수 있다.
Entity
Entity는 JPA의 영속성 컨텍스트 (persistence context)에 의해 관리되는 대상이다. Entity의 경우 @NoArgsConstructor
로 기본 생성자를 만들어준다. -> why? JPA에서 Lazy 로딩시에 proxy 객체를 만드는데, 이때 기본 생성자를 사용한다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Post {
@Id @GeneratedValue
private Long id;
private String title;
private String content;
public Post(String title, String content) {
this.title = title;
this.content = content;
}
public static Post createPost(String title, String content){
return new Post(title, content);
}
public void update(String title, String content){
this.title = title;
this.content = content;
}
}
Controller
@RestController
: @Controller
에 @ResponseBody
가 추가되어, JSON이나 XML 같은 data를 return 해줄 수 있다. HttpMessageConverter
가 객체를 Data로 바꾸어 return하게 된다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
* @since 4.0.1
*/
@AliasFor(annotation = Controller.class)
String value() default "";
}
- Controller에서는 기본적으로 URL Mapping을 해주는 역할을 한다.
- Controller에서 HTTP Method에 맞춰서
@~Mapping
을 사용하여, 어떤 HTTP method를 사용할지 명시할 수 있습니다. 해당 method가 아닌 다른 HTTP 요청을 보낼 경우405 Method Not Allowed
Error가 발생한다. @GetMapping
,@PostMapping
,@PatchMapping
,@PutMapping
,@DeleteMapping
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1")
public class PostController {
private final PostService postService;
@GetMapping("/post/{postId}")
public PostResponseDTO getPost(
@PathVariable("postId") Long id
) {
return postService.getPost(id);
}
@PostMapping("/post")
public String createPost(
@RequestBody PostRequestDto postRequestDto
) {
postService.create(postRequestDto);
return "OK";
}
@PatchMapping("/post/{postId}")
public String updatePost(
@PathVariable("postId") Long id,
@RequestBody PostUpdateRequestDto postUpdateRequestDto
) {
postService.update(id, postUpdateRequestDto);
return "OK";
}
@DeleteMapping("/post/{postId}")
public String deletePost(
@PathVariable("postId") Long id
) {
postService.delete(id);
return "OK";
}
}
위 코드에서 생성자로 의존성 주입을 하기 위해 생성자가 필요한데, Lombok을 이용해서, 생성자를 @RequiredArgsConstructor
로 대체할 수 있다.
DTO (Data Transfer Object)
- DTO는 Data 교환 객체를 의미한다.
- Domain을 그대로 노출시키면, 위험할 수도 있다. Domain 자체가 서비스에서 중요한 data이기 때문에, Domain을 API에 응답으로 직접 노출시키는 것이 아니라, DTO 객체를 만들어 DTO 객체에 값을 담아 return 해준다. Controller에 각각 method에 return type을 보면 DTO를 사용하는 것을 알 수 있다.
@Data
public class PostRequestDto {
private String title;
private String content;
}
Lombok의 @Data
Annotation은 Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode, Value를 포함하고 있다. 따라서 DTO class에 붙여서 사용하는데, 너무 많은 method가 붙어있다고 판단되면 필요한 것만 붙여서 사용해도 괜찮을 것 같다.
또한 JDK 14에서 나온 record를 이용하여 DTO를 정의할 수 있다.
public record PostUpdateRequestDto(
String title,
String content
){
}
Service
@Transactional
: Spring Boot에서 Transaction을 보장하기 위하여 달아주는 annotation입니다.
(TODO : Spring Transaction 관리 방법에 대해서는 나중에 정리)
@Service
@Transactional
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
@Transactional
public void create(PostRequestDto postRequestDto) {
Post post = new Post(postRequestDto.getTitle(), postRequestDto.getContent());
postRepository.save(post);
}
@Transactional(readOnly = true)
public PostResponseDTO getPost(Long id) {
Post post = postRepository.findById(id).get();
return new PostResponseDTO(post.getId(), post.getTitle(), post.getContent());
}
@Transactional
public void update(Long id, PostUpdateRequestDto postUpdateRequestDto) {
Post post = postRepository.findById(id).get();
post.update(postUpdateRequestDto.title(), postUpdateRequestDto.content());
postRepository.save(post);
}
@Transactional
public void delete(Long id) {
postRepository.deleteById(id);
}
}
Repository
Repository 부분에서 JPA를 이용하여 DB와의 통신이 진행된다.
JPA에서는 기본적으로 많이 사용하는 SQL query들을 method로 구현해놓은 JpaRepository를 지원한다. 따라서 JpaRepository를 이용해서 편리하게 구현을 할 수 있다.
또한 JPQL(Java Persistence Query Language) 또는 SQL을 이용해 Query 문을 작성하여, method를 구현할 수 있다.
@Repository
@RequiredArgsConstructor
public class PostCustomRepository {
@PersistenceContext
private EntityManager em;
public void save(Post post) { em.persist(post);}
public Post findOne(Long id) {
return em.find(Post.class, id);
}
public List<Post> findAll() {
return em.createQuery("select p from Post p", Post.class)
.getResultList();
}
}
public interface PostRepository extends JpaRepository<Post, Long> {
}