Spring에 관한 첫번째 글이다. 졸업 작품으로 '자동 스케줄링 시스템'을 개발하게 됐다.
거기서 REST API 서버를 담당하게 됐다.
전에 Spring을 배운적은 있지만, 오래돼서 기억들이 많이 소실됐다...
그렇다고 다시 책을 읽으면서 공부하기는 싫어서 프로젝트를 진행하면서 하게되는 부분들을 그때 그때 찾고 블로그에 정리를 해보려고 한다.

일단 Spring Boot를 이용해서 프로젝트를 시작하려고 한다.
그래서 먼저 https://start.spring.io/ 에서 프로젝트를 생성했다!

라이브러리로 Spring Web, Lombok, Thymeleaf를 추가해주고 프로젝트를 생성해주었다.
REST API 개발을 하려고 하는 것이기 때문에 간단한 데이터를 만들어서 해당 데이터를 JSON 형식으로 불러오는 웹페이지를 구현하려고 했다.
package api.mentalotus.Controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class TestController {
@GetMapping("hello-string")
@ResponseBody
public String helloString(@RequestParam("name") String name) {
return "hello " + name;
}
}
사용한 어노테이션을 설명하면,
@Controller는 해당하는 클래스가 컨트롤러임을 인식시켜주고 Bean으로 등록을 합니다. 현재 RestAPI를 개발하는 것이기 때문에 @RestController를 사용해주어도 됩니다. 이 경우 @ResponseBody 어노테이션은 사용하지 않아도 됩니다.
@GetMapping은 HTTP Get 요청에 응답 메소드를 매핑해주는 역할을 합니다. 여기서 파라미터로 "hello-string"이 왔는데, url 주소의 엔드포인트가 hello-string일 때 매핑을 진행합니다.
@ResponseBody는 HTML에 body태그에 return 값을 넣어줍니다.
@RequestParam는 HTTP GET 요청은 endpoint 뒤에 '?' 와 함께 파라미터가 붙어서 임의의 값을 받아 올 수 있는데, 여기서 해당 파라미터의 이름을 정의 해주는 역할을 합니다.
다음은 전에 모델링한 데이터를 토대로 POST 요청을 받아오는 테스트를 해보려고 했습니다. 그러기 위해서 먼저 데이터를 정의해야 했습니다.

두개의 모델링 데이터를 토대로 모델을 코딩을 했습니다.
* User *
package api.mentalotus.Model;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
private String user_key;
private String nickname;
private double updated;
}
* Schedule *
package api.mentalotus.Model;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Schedule {
private String id;
private String user_key;
private String title;
private String comment;
private LocalDateTime start;
private LocalDateTime end;
private String location;
private String Formalize_time(LocalDateTime x)
{
String formatted = x.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
return formatted;
}
private void setStart(int y, int m, int d, int h, int min)
{
start = LocalDateTime.of(y, m, d, h, min);
}
private void setEnd(int y, int m, int d, int h, int min)
{
end = LocalDateTime.of(y, m, d, h, min);
}
}
위의 코드를 보면 @Data라는 어노테이션이 보입니다. 이 어노테이션은 lombok에서 제공하는 Getter, Setter 와 Equals, HashCode 등의 기능을 제공해줍니다.
그다음은 NoArgsConstructor, AllArgsConstructor 어노테이션이 보입니다. 이 어노테이션은 문자 그대로 인수가 없는 생성자와, 모든 속성에 해당하는 인수를 갖는 생성자를 정의합니다.
이제 이렇게 만든 모델을 토대로 POST 요청을 받으면 mongoDB에 데이터를 저장하게끔 하겠습니다.
일단 기본적으로 mongoDB를 사용을 해봤었기 때문에, ID를 만들 필요는 없었습니다. 기존에 사용하던 몽고 클라우드의 데이터베이스를 다 지웠습니다.

그리고 mongoDB Compass를 이용하여 주소 및 키를 입력하고 Collection을 정의했습니다.
mongoDB 의존성 추가
build.gradle 파일에 들어가서 아래와 같이
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' 를 작성합니다.

application.yml에 mongo atlas 연결을 위한 내용을 작성 한다.
spring:
data:
mongodb:
uri: mongodb+srv://<유저이름>:<비밀번호>@<클러스터주소>/<데이터베이스이름>?retryWrites=true&w=majority
이 yml 파일 설정은 외부 설정 시 인수를 넣어주는데 사용을 한다.
예시로 어떠한 데이터를 Converter를 하기 위해서 Jackson을 사용 한다하면,
jackson:
serialization:
WRITE_DATES_AS_TIMESTAMPS: false
위와 같이 내용을 추가 할 수 있다.
다음 할일은 MongoRepository를 extends 하는 Repository 인터페이스를 만들어주어야 한다. 이 때, MongoRepository의 제네릭에는 참고할 Domain 이름과 그 도메인의 식별키의 타입을 제네릭으로 넣어준다.
이렇게 생성한 인터페이스 위에 Component 어노테이션을 작성한다.
@Component 어노테이션은 스프링 프레임워크에서 컴포넌트 스캔을 통해 스프링 빈으로 등록할 클래스에 붙이는 어노테이션입니다. 스프링 컨테이너가 이 어노테이션이 붙은 클래스를 스캔하여 해당 클래스의 인스턴스를 스프링 빈으로 등록하게 됩니다.
@Component 어노테이션은 다음과 같은 하위 어노테이션들도 포함합니다.
@Controller: MVC에서 컨트롤러 역할을 하는 클래스에 사용합니다.
@Service: 비즈니스 로직을 담당하는 서비스 클래스에 사용합니다.
@Repository: DAO(Data Access Object) 역할을 하는 클래스에 사용합니다.
@Configuration: 스프링 설정 파일 역할을 하는 클래스에 사용합니다.
@Component 어노테이션은 이 외에도 다양한 어노테이션과 함께 사용될 수 있습니다. 이를 통해 스프링 컨테이너는 다양한 빈을 스캔하고 등록할 수 있습니다.
<--ScheduleRepository.java-->
package api.mentalotus.Repository;
import api.mentalotus.Domain.Schedule;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public interface ScheduleRepository extends MongoRepository<Schedule, String> {
List<Schedule> findByUserkey(String userkey);
}
그래서 생성한 Repository의 결과가 이것이다.
위의 보면 findByUserKey라는 메소드가 존재하는데, 선언부만 존재할 뿐 구현부가 존재하지 않는다. 이 구현부는 Spring Data MongoDB와 같은 Spring Data 프로젝트 에서는 Repository의 메소드를 자동으로 구현해준다. 따라서 위와같이 이름과 인수를 도메인의 속성에 맞춰서 잘 작성해주면 알아서 만들어준다.
MongoDB에서는 LocalDateTime의 데이터를 자동적으로 바꿔주지 않습니다. 따라서 이를 Jackson을 이용하여 Converter를 해주어야 하는데, 이 부분은 나중에 해주고 지금은 Controller의 PostMapping 어노테이션을 추가하여 확인하는 것을 목표로 하기 위하여 start와 end 속성ㅡ이 타입을 String으로 변경하고 진행했습니다.
그리고 MongoDB의 Document와 Domain을 매핑하기 위하여 @Document 어노테이션을 도메인 클래스 위에 작성을 해줍니다.
package api.mentalotus.Domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
@Data
@Document("Schedule")
@NoArgsConstructor
@AllArgsConstructor
public class Schedule {
@Id
private String id;
private String userkey;
private String title;
private String comment;
@Field("start")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private String start;
@Field("end")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private String end;
private String location;
}
위에서나온 @JsonFormat은 Jackson라이브러리에 있는 어노테이션으로 옆에 pattern과 같은 형식의 Json value를 확인하게 되면 아래의 정의한 속성의 타입으로 변환을 해준다. 근데 지금은 start가 String이기 때문에 무의미하다. 하지만, 이가 Date 타입으로 변경 시에 유용하기에 작성만 해주고 후에 쓰기 위해 남겨두었다.
Service는 Repository를 다루는 클래스라고 생각하면 좋다. @Service 어노테이션을 작성해 줌으로써 자바 컨테이너의 빈으로 등록된다. 그리고 Repository를 속성으로 선언하고 이를 인수로 갖는 생성자를 선언해줍니다. 그리고 Autowired 어노테이션을 작성하여 Repository에 해당하는 빈을 주입 받습니다. 그리고 Repository에서 선언했던 메소드들을 이용하여 Repository에 데이터를 저장하고 읽는 기능을 수행 할 수 있습니다.
package api.mentalotus.Service;
import api.mentalotus.Domain.Schedule;
import api.mentalotus.Repository.ScheduleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ScheduleService {
private ScheduleRepository scheduleRepository;
@Autowired
public ScheduleService(ScheduleRepository scheduleRepository)
{
this.scheduleRepository = scheduleRepository;
}
public void save(Schedule schedule){
scheduleRepository.save(schedule);
}
public List<Schedule> findUserSchedules(String userKey)
{
return scheduleRepository.findByUserkey(userKey);
}
}
이렇게 해서 mongoDB와 관련된 모든 설정을 맞출 수 있었습니다. 그다음은 Controller에서 Post 요청을 받아들이는 부분을 작성해 보겠습니다.
package api.mentalotus.Controller;
import api.mentalotus.Domain.Schedule;
import api.mentalotus.Service.ScheduleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController // controller임을 알려주는 표시
// 이곳으로 들어오는 API주소를 mapping, /api주소로 받겠다(localhost:8080/api)
public class ScheduleController {
// @RequestMapping(method = RequestMethod.POST, path = "/postMethod") // 아래랑 동일
private final ScheduleService scheduleService;
@Autowired
public ScheduleController(ScheduleService scheduleService) {
this.scheduleService = scheduleService;
}
@PostMapping(path = "/schedule", consumes = "application/json")
public void save(@RequestBody Schedule schedule)
{
scheduleService.save(schedule);
}
}
GET을 했던 것과 같이 @RestController 어노테이션을 작성하여 자바컨테이너에 bean으로 등록을 합니다.
그리고 Controller에서는 Repository를 다루는 서비스를 이용할 것이기 때문에 Service를 final로 선언해줍니다. 그리고 Autowired 어노테이션을 통해서 컨트롤러가 생성이 될 때, scheduleService가 주입되게끔 해줍니다. 그리고 @PostMapping 어노테이션을 통하여 Post 요청을 받을 수 있게 해줍니다. 여기서 어트리뷰트로 사용된 consumes = "application/json"은 이러한 데이터가 들어올때만 반응하겠다는 것을 의미합니다. 이 때 주의해야 할 점은 POST 메소드를 postman을 통해서 테스트 할때 헤더 부분에 "Content-Type" : "application/json"을 꼭 써주어야 합니다.
Postman을 통해서 JSON 형식의 데이터를 POST 바디에 담아 요청을 해주었다.

그 결과 위와 같이 mongoDB에 잘 저장된 모습을 확인 할 수 있었다.