개발이 취미인 사람

[Spring Boot] Spring Boot API 만들기 (3) - @Controller, @Service, @Repository 구조 설정 본문

백앤드(Back-End)/Spring Boot

[Spring Boot] Spring Boot API 만들기 (3) - @Controller, @Service, @Repository 구조 설정

RyanSin 2022. 4. 24. 13:10
반응형

- 지난 시간

안녕하세요. 지난 시간에는 계층 구조(Layered Architecture)에 대해 알아봤습니다.

 

혹시 놓치고 오신 분들은 아래 링크를 통해 학습하고 오시는 걸 추천드리겠습니다.

[Spring Boot] Spring Boot API 만들기 (2) - 계층 구조 설명

 

[Spring Boot] Spring Boot API 만들기 (2) - 계층 구조 설명

- 지난 시간 안녕하세요. 지난 시간에는 Spring Boot 프로젝트를 생성하는 시간을 가져봤습니다. 혹시 놓치고 오신 분들은 아래 링크를 통해 프로젝트를 생성하고 오시는 걸 추천드리겠습니다. [Spri

any-ting.tistory.com

 

- 개요

이번 시간에는 실제 Spring Boot 프로젝트 구조를 만들어 보는 시간을 가져보겠습니다.

 

지난 시간에 저희는 계층 구조(Layered Architecture)를 배웠습니다. Spring Boot에는 아래와 같은 계층 구조가 존재합니다.

 

Spring Boot 계층 구조

  1. Controller(컨트롤러)
    컨트롤러 계층은 HTTP 요청과 요청된 정보를 체크하며, 인증을 담당합니다.

    기본적인 계층 구조에서는 비즈니스 계층이 유효성 검사를 하지만 Spring Boot 프로젝트에서는 Validation 라이브러리를 활용해서 요청 정보를 바로 체크할 수 있습니다.

  2. Service(서비스)
    서비스 계층은 비즈니스 로직을 담당하는 계층입니다. 기본적인 개념과 다른 부분은 없습니다.

  3. Domain(도메인)
    도메인 계층은 실제 데이터베이스 테이블 정보를 가지는 하나의 Entity 클래스를 생성하고 해당 Entity를 컨트롤합니다.

  4. Repository(리포지토리)
    리포지토리에서는 실제 데이터베이스에 쿼리문을 실행하는 로직을 담당합니다.

 

자, 그럼 Controller부터 차근차근 생성해 보겠습니다.

 

가장 먼저 아래처럼 패키지 폴더를 생성합니다.

 

프로젝트 구조

 

위와 같이 우리는 하나의 User 도메인 구조를 만들었습니다.(객체지향 시간은 아니기 때문에 객체지향 개념은 나중에 적용하겠습니다.)

 

- Controller

기본적으로 Controller를 두 가지 어노테이션(Annotation)이 있습니다.

  1. @Controller
  2. @RestController

@Controller 어노테이션은 전통적인 Spirng MVC에서 사용할 때 사용됩니다.

특정 웹 사이트 페이지 모델을 만들고 반환할 때 사용되며, 반대로 @RestController는 RESTFul API 서버를 만들 때 사용됩니다.

 

둘의 차이는 ResponseBody가 있고 없는 게 가장 큰 차이입니다.

 

- @RestController -

@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 "";

}

 

@RestController 어노테이션은 @Controller와 @ResponseBody 어노테이션이 있는 걸 알 수 있습니다.

 

즉! @RestContoller 어노테이션은 @Controller + @ResponseBody 어노테이션이 합쳐진 어노테이션이라고 생각하시면 됩니다.

 

그럼 @Controller는 어떤 구조를 가지고 있을까요?

 

- @Controller -

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {

	/**
	 * 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)
	 */
	@AliasFor(annotation = Component.class)
	String value() default "";

}

@Controller 어노테이션은 @ResponseBody 어노테이션이 있지 않습니다.

 

RESTFul API 서버를 만들어 운영하면 기본적으로 페이지 정보를 클라이언트에게 반환하는 게 아닌 요청에 따른 필요한 정보를 반환합니다. JSON 객체 형태 안에 필요한 데이터를 담아서 반환합니다.

 

그렇기 때문에 RESTFul API를 개발할 때는 @RestController를 사용합니다. Controller 패키지 안에 UserController 자바 파일을 생성하고  코드를 작성해 보겠습니다.

 

- 소스 코드

@Controller

package com.ryan.spring_boot_rest_api.controller;

import com.ryan.spring_boot_rest_api.dto.CreatUserDto;
import com.ryan.spring_boot_rest_api.dto.SuccessResponse;
import com.ryan.spring_boot_rest_api.dto.UpdateUserDto;
import com.ryan.spring_boot_rest_api.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.nio.charset.Charset;

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    /**
     * @author Ryan
     * @description 유저 생성 컨트롤러
     *
     * @path /user/create
     *
     * @return User Id
     */
    @PostMapping(value = "/create")
    public ResponseEntity<SuccessResponse> onCreateUser(@RequestBody @Valid CreatUserDto creatUserDto){

        SuccessResponse response = this.userService.onCreateUser(creatUserDto);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));

        return new ResponseEntity<>(response, headers, HttpStatus.CREATED);
    }
    
}

 

위 소스코드에서 중요한 다섯 가지 항목이 있습니다.

  1. @RequestMapping("/user") : 해당 컨트롤러에 루트 라우트 경로를 나타냅니다.
  2. @RequiredArgsConstructor : 생성자 주입방식 통한 의존관계 주입(의존관계에 대한 개념은 나중에 글을 작성하겠습니다. :>)
  3. @PostMapping : HTTP Method 설정, value는 해당 Method에 경로는 설정합니다. 
  4. @RequestBody : @RequestBody는 HTTP 요청 시 Body 정보를 받기 위해 사용됩니다.
  5. @Valid : validation 라이브러리를 선언해서 사용하는 어노테이션입니다. RequestBody 데이터를 유효성 검사할 때 사용합니다.

HTTP RequestBody 형식에 대해 알아봤습니다. 그렇다면 Query Params 방식과 Path Variables 방식은 어떻게 할까요?

 

제가 생각할 때는 Spring 프레임워크는 어노테이션을 잘 알고 잘 사용해야 한다고 생각합니다. (정말... 잘 만들어 놓은 프레임워크 같아요!)

 

개발자가 하나하나 만들어서 사용하는 게 아니 @... 하는 어노테이션만 선언해서 사용하면 원하는 프로그램이 잘 동작하기 때문에 정말 비즈니스 로직에 집중할 수 있는 것 같아요.

 

그럼 나머지 Query Params 방식과 Path Variables 방식에 대해서 설명하겠습니다.

 

/**
 * @author Ryan
 * @description 단일 유저 조회 (Query Params 방식)
 *
 * @path /user/user?id=
 *
 * @return User
 */
@GetMapping(value = "/user")
public ResponseEntity<SuccessResponse> getUser(@RequestParam("id") int id){

    SuccessResponse response = this.userService.getUser(id);

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));

    return new ResponseEntity<>(response, headers, HttpStatus.OK);
}

위에서 우리가 알아야 할 부분은 @RequstParam("id") 어노테이션입니다.

 

@RequstParam 어노테이션을 선어 하면 해당 변수는 Query Params 방식으로 사용됩니다.

 

/**
 * @author Ryan
 * @description 특정 유저 삭제 (Path Variables 방식)
 *
 * @path /user/delete/{id}
 *
 * @return Boolean
 */
@DeleteMapping(value = "/delete/{id}")
public ResponseEntity<SuccessResponse> deleteUser(@PathVariable("id") int id){

    SuccessResponse response = this.userService.deleteUser(id);

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));

    return new ResponseEntity<>(response, headers, HttpStatus.OK);
}

 

이번에는 Path Variables 방식을 사용하기 위해서 @PathVariable 어노테이션을 선언했습니다.

 

Controller 부분에서 제일 중요한 부분은 클라이언트 요청에 대한 유효성 검사와 인증 부분입니다. (꼭 기억해주세요 :>)

 

그리고 각각에 HTTP Method는 어노테이션을 선언해서 사용하면 됩니다.

 

@PostMapping
@GetMapping
@PutMapping
@PatchMapping
@DeleteMapping ...

 

 

다음으로는 Service 부분에 대해 설명하겠습니다. 서비스는 각 비즈니스 로직을 담당합니다. 그렇기 때문에 작성하고 싶은 로직을 작성하시면 됩니다.

 

예를 들어 하나의 유저를 등록한다고 가정해보겠습니다. (복잡한 로직이 아닌 간단한 로직이라고 생각하고 설명하겠습니다.)

 

가장 먼저 유저가 DB에 존재 여부를 체크하겠죠?(중복검사)

 

중복 검사 결과에 따라 두 가지 경우가 발생할 수 있습니다.

  1. 유저가 있다면, "중복되는 유저입니다."라는 정보를 클라이언트에게 반환합니다.
  2. 유저가 없다면, "새로운 유저를 등록했습니다."라는 정보를 클라이언트에게 반환합니다.

위와 같은 로직을 작성하고 처리하는 부분이 @Service 영역입니다.

 

@Service

package com.ryan.spring_boot_rest_api.service;

import com.ryan.spring_boot_rest_api.domain.User;
import com.ryan.spring_boot_rest_api.dto.CreatUserDto;
import com.ryan.spring_boot_rest_api.dto.SuccessResponse;
import com.ryan.spring_boot_rest_api.dto.UpdateUserDto;
import com.ryan.spring_boot_rest_api.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    /**
     * @author Ryan
     * @description 유저 생성 컨트롤러
     *
     * @path /user/create
     *
     * @return User Id
     */
    public SuccessResponse onCreateUser(CreatUserDto creatUserDto){

        int id = creatUserDto.getId();
        String name = creatUserDto.getName();

        int userId = this.userRepository.save(id, name);

        return new SuccessResponse(true, userId);
    }


    /**
     * @author Ryan
     * @description 전체 유저 조회
     *
     * @path /user/list
     *
     * @return User[]
     */
    public SuccessResponse getUserList(){
        List<User> userList = new ArrayList<>(this.userRepository.findAllByUser());
        return new SuccessResponse(true, userList);
    }

    /**
     * @author Ryan
     * @description 단일 유저 조회
     *
     * @path /user/:id
     *
     * @return User
     */
    public SuccessResponse getUser(int id){
        User byUser = this.userRepository.findByUser(id);

        return new SuccessResponse(true, byUser);
    }

    /**
     * @author Ryan
     * @description 유저 정보 수정
     *
     * @path /update
     *
     * @return User Id
     */
    public SuccessResponse updateUser(UpdateUserDto updateUserDto){

        int id = updateUserDto.getId();
        String name = updateUserDto.getName();

        User user = this.userRepository.update(id, name);

        return new SuccessResponse(true, user);
    }


    /**
     * @author Ryan
     * @description 특정 유저 삭제
     *
     * @path /delete
     *
     * @return Boolean
     */
    public SuccessResponse deleteUser(int id){

        Boolean success = this.userRepository.delete(id);

        return new SuccessResponse(success, null);
    }
}

 

위에서 소스코드에서 우리가 잘 알아야 하는 부분은 로직과 @Service는 Repository와 Domain에 의존한다는 부분입니다.

 

지금은 이해가 잘 가시지 않을 테지만 Spring Boot 프레임워크를 사용해서 조금씩 개발을 하다 보면 지금에 말을 이해하실 수 있으니 걱정하지 마세요. :)

 

마지막으로 @Repository에 대해서 설명하겠습니다.

 

@Repository는 Databases CRUD와 같은 부분을 담당합니다. (현재 실제 DB가 아닌 메모리에서 데이터를 저장하고 추출하는 Map 클래스를 사용했습니다.)

@Repository

package com.ryan.spring_boot_rest_api.repository;

import com.ryan.spring_boot_rest_api.domain.User;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Repository
public class UserRepository {

    Map<Integer, User> memoryDB = new ConcurrentHashMap<>();

    /**
     * @author Ryan
     * @description 유저 등록
     *
     * @param id 유저 아이디
     * @param name 유저 이름
     *
     * @return id 유저 아이디
     */
    public int save(int id, String name){

        User user = new User();
        user.setId(id);
        user.setName(name);

        this.memoryDB.put(id, user);

        return id;
    }

    /**
     * @author Ryan
     * @description 단일 유저 조회
     *
     * @return User 유저
     */
    public User findByUser(int id){
        return this.memoryDB.get(id);
    }

    /**
     * @author Ryan
     * @description 유저 정보 전체 조회
     *
     * @return User[] 유저 리스트
     */
    public Collection<User> findAllByUser(){
        return this.memoryDB.values();
    }


    /**
     * @author Ryan
     * @description 유저 수정
     *
     * @return User 유저
     */
    public User update(int id, String name){

        User user = this.memoryDB.get(id);
        user.setName(name);

        return user;
    }


    /**
     * @author Ryan
     * @description 유저 삭제
     *
     * @param id 유저 고유 아이디
     *
     * @return Boolean 성공 여부
     */
    public Boolean delete(int id){

        User user = this.memoryDB.remove(id);

        //삭제 하려는 유저가 없다면 에러 처리
        if(user == null){
            return false;
        }

        return true;
    }

}

 

마지막으로 도메인 (DB 모델)에 대해 설명하겠습니다.

@Data
@NoArgsConstructor
public class User {
    public int id; //유저 아이디
    public String name; // 유저 이름
}

현재는 특정 JPA 같은 ORM을 사용하지 않기 때문에 위와 같이 임시로 작성해서 사용합니다.

 

하지만 실제 Domain 부분은 DB 모델 정보를 작성해서 사용합니다. (JPA 시간에 자세하게 작성하겠습니다.)

 

이번 시간에는 기본적인 Spring Boot 구조를 설정하는 시간을 가져봤습니다. 혹시 어려운 부분이 있다면 댓글을 남겨주세요.

 

- GitHub

https://github.com/Ryan-Sin/Spring_Boot_Rest_API/tree/v1

 

GitHub - Ryan-Sin/Spring_Boot_Rest_API: Spring Boot RESTful API 프로젝트

Spring Boot RESTful API 프로젝트. Contribute to Ryan-Sin/Spring_Boot_Rest_API development by creating an account on GitHub.

github.com