기초 CRUD를 위한 backend 구현 중 사용한 spring boot 어노테이션을 소개하는 시간을 가져보겠습니다. 예시로 사용한 코드는 개인적으로 toy-project로 사용했던 todo webpage 제작한 코드를 활용했습니다. 이는 github에 올라와 있으니 참고하실 분은 url을 참고하시길 바랍니다. 개인 github/toy-todo-back
package com.everything.todo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
SpringBootApplication 어노테이션은 해당 클래스가 스프링부트를 설정하는 클래스임을 의미합니다. 스프링에서는 이 어노테이션이 달린 클래스가 있는 패키지를 베이스 패키지로 간주합니다.
우리가 스프링을 사용하는 가장 큰 이유 중 하나는 의존성 주입 컨테이너로써의 역할 때문입니다. 스프링은 베이스 패키지와 그 하위 패키지에서 자바 빈(Bean)을 찾아 스프링의 의존성 주입 컨테이너 오브젝트(ApplicationContext)에 등록하게 됩니다. 그리고 애플리케이션 실행 중 어떤 오브젝트가 필요한 경우, 의존하는 다른 오브젝트를 찾아 연결해주죠.
스프링이 개발자 대신 객체를 제어하기 위해서는 객체들이 빈(Bean)으로 등록되어 있어야 하죠. 이를 스프링 어노테이션으로 간단하게 작업할 수 있습니다.
스프링 MVC에서는 @Controller 등으로 빈으로 등록할 수 있으면 config와 관련된 객체들은 두가지 방법으로 객체를 빈으로 등록할 수 있습니다. config 관련 객체를 등록하는 첫번째 방법이 @Component입니다.
@Component
public class Utility {
// ...
}
@Component는 클래스 레벨에서 선언함으로써 스프링이 런타임시에 컴포넌트 스캔을 하여 자동으로 빈을 찾고 등록하도록 하는 어노테이션입니다. 편리해 보이지만 모든 경우에 자동화가 정답인 것은 아닙니다.
엔터프라이즈 애플리케이션(Enterprise Application;EA, 비지니스 또는 정부와 같은 회사 환경에서 작동하도록 설계된 대규모 SW 시스템 플랫폼)의 경우 엔지니어가 오브젝트를 생성하고 사용하는 경로를 정확히 알아야 하는 경우가 많습니다.
또한 다른 라이브러리를 사용하는데, 이 라이브러리 클래스가 스프링 기반이 아니라서 @Component를 추가하지 못하는 경우도 있죠. 이런 경우에는 스프링으로 빈을 관리하기 위해 직접 스프링에게 작업 지시를 해야할 경우가 생깁니다. 따라서 두번째 방법은 @Bean입니다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl();
}
}
@Bean 어노테이션은 위의 예시와 같이 메소드 레벨에서 선언하며, 반환되는 객체(인스턴스)를 엔지니어가 직접 '이 빈은 이렇게 생성해라'하고 말해줄 필요가 있습니다. 다시 말해 @Bean을 이용해 우리는 스프링에게 이 오브젝트를 정확히 어떻게 생성해야 하는지, parameter를 어떻게 넣어줘야 하는지 알려줘야 합니다.
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoDTO {
...
public TodoDTO(final TodoEntity entity) {
...
}
}
@Builder는 오브젝트 생성을 위한 디자인 패턴 중 하나입니다. 롬복(lombok, Java 라이브러리로 반복되는 메서드 작성 코드를 줄여주는 코드 다이어트 라이브러리)이 제공하는 @Builder 어노테이션을 사용하면 따로 builder 클래스를 개발하지 않아도 Builder 패턴을 사용하여 오브젝트를 생성할 수 있습니다.
@NoArgsConstructor 어노테이션은 매개변수가 없는 생성자를 구현해줍니다.
반대로 @AllArgsConstructor 어노테이션은 클래스의 모든 멤버변수를 매개변수로 받는 생성자를 구현해줍니다.
@Data 어노테이션은 클래스 멤버 변수의 Getter/Setter 메서드를 구현해줍니다.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("test") //리소스
public class TestController {
@GetMapping
public String testController() {
return "Hello World!";
}
@GetMapping("/testGetMapping")
public String testControllerWithPath() {
return "Hello World! testGetMapping ";
}
...
}
REST API를 구현하기 위해 @RestController 어노테이션을 이용하여 해당 컨트롤러가 RestControlleer임을 명시해주어야 합니다. 이 어노테이션은 http 관련 코드 및 요청/응답 매핑을 스프링이 알아서 해주도록 합니다.
또한 우리는 특정 uri로 요청을 보내면 Controller에서 어떤 방식으로 처리할지를 정의합니다. 이때 들어온 요청을 특정 메서드와 매핑하기 위해서 사용되는 것이 @RequestMapping입니다. 공통적인 url은 class에 @RequestMapping으로 설정한다.
@GetMapping 어노테이션을 이용해 메서드의 리소스와 HTTP 메서드를 지정할 수 있습니다. 위의 코드의 경우 클라이언트가 "test" 리소스에 대해 Get 메서드로 요청하면, @GetMapping에 연결된 컨트롤러가 실행됩니다.
@GetMapping("/testGetMapping") 어노테이션을 통해서 /test/testGetMapping이 이 메서드에 연결되었다는 사실을 알 수도 있습니다.
@GetMapping과 비슷한 어노테이션으로 @PostMapping, @PutMapping, @DeleteMapping이 있는데, 각각 HTTP 메서드 POST, PUT, DELETE를 의미합니다.
@RestController
@RequestMapping("test") //리소스
public class TestController {
@GetMapping("/{id}")
public String testControllerWithPathVariables(@PathVariable(required = false) int id) {
return "Hello World! ID " + id;
}
// /test경로는 이미 존재하므로 /test/testRequestParam으로 지정했다.
@GetMapping("/testRequestParam")
public String testControllerRequestParam(@RequestParam(required = false) int id) {
return "Hello World! ID " + id;
}
// /test경로는 이미 존재하므로 /test/testRequestBody로 지정했다.
@GetMapping("/testRequestBody")
public ResponseDTO<String> testControllerRequestBody(@RequestBody TestRequestBodyDTO testRequestBodyDTO) {
List<String> list = new ArrayList<>();
list.add("Hello World! I'm ResponseDTO");
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return response;
}
@PathVariable을 이용하면 /{id}와 같이 URI의 경로로 넘어오는 값을 변수로 받아 올 수 있습니다. 매개변수 /{id}는 경로로 들어오는 임의의 숫자 또는 문자를 변수 id에 매핑하라는 뜻입니다. (required = flase)는 이 매개변수가 꼭 필요한 것은 아니라는 뜻입니다. 따라서 test/뒤에 id가 명시되지 않더라도 에러는 나지 않을 것입니다.
@RequestParam과 @RequestBody는 위의 @PathVariable과 동일한 동작을 수행합니다. 다만 @RequestBody는 보통 반환하고자 하는 리소스가 복잡할 때 사용합니다. 예를들어서 String이나 int같은 기본 자료형이 아닌 오브젝트처럼 복잡한 자료형을 통째로 요청에 보내고 싶은 경우가 이에 해당합니다.
@RestController
@RequestMapping("todo")
public class TodoController {
}
@RestController는 안을 들여다보면 크게 두 어노테이션으로 이루어져 있습니다. 하나는 Controller이고 하나는 ResponseBody입니다.
@Controller
@RespnseBody
public @interface RestController{
...
}
@Controller는 다시 @Component로 스프링이 이 클래스의 오브젝트를 자동으로 생성하고 다른 오브젝트들과의 의존성을 연결합니다. @ResponseBody는 이 클래스의 메서드가 리턴하는 것은 웹 서비스의 ResponseBody라는 뜻입니다. 다시 말해, 메서드가 리턴할 때 스프링은 리턴된 오브젝트를 JSON 형태로 바꾸고 HttpResponse에 담아 반환하게 됩니다.
위 방법으로 문자열보다 복잡한 오브젝트 같은 형태를 리턴할 수 있다.
@Service
public class TodoService {
public String testService() {
...
}
}
@Service 어노테이션은 스테레오타입 어노테이션입니다. 어노테이션 내부에 @Component 어노테이션을 가지고 있습니다. 스프링 컴포넌트이며 기능적으로는 비지니스 로직을 수행하는 서비스 레이어임을 알려주는 어노테이션입니다.
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "Todo")
public class TodoEntity {
@Id
@GeneratedValue(generator="system-uuid")
@GenericGenerator(name="system-uuid", strategy = "uuid")
private String id; // 이 오브젝트의 아이디
private String userId; // 이 오브젝트를 생성한 유저의 아이디
private String title; // Todo 타이틀 예) 운동 하기
private boolean done; // true - todo를 완료한 경우(checked)
}
JPA관련 어노테이션을 이용해 자바 클래스를 엔티티로 정의할 때 몇가지 주의해야 할 점이 있습니다.
- 클래스에는 매개변수가 없는 생성자, NoArgsContructor가 필요합니다.
- Getter/Setter가 필요합니다.
- 기본키를 지정해줘야 합니다.
자바 클래스를 엔티티로 지정하기 위해서는 위의 코드처럼 TodoEntity에 @Entity를 추가해야 합니다. 엔티티에 이름을 부여하고 싶다면 @Entity("TodoEntity")처럼 매개변수를 넣어 줄 수 있습니다.
테이블 이름을 지정하기 위해서 해당 어노테이션을 삽입해야 합니다. 이 엔티티는 데이터베이스의 parameter로 명시한 테이블에 자동으로 맵핑될 것입니다. 위의 코드의 경우 데이터베이스의 Todo 테이블에 매핑됩니다.
만약 @Table을 추가하지 않거나, 추가해도 name을 명시하지 않는다면 @Entity의 이름을 테이블 이름으로 간주하게 됩니다.
@Entity에도 이름이 지정되어 있지 않다면 클래스의 이름을 테이블 이름으로 간주하게 됩니다.
@Id는 기본키가 될 필드에 지정합니다. 위 코드의 경우 id가 기본 키 이므로 id 필드 위에 @Id를 추가해야 합니다. Id 필드는 오브젝트를 데이터베이스에 저장할 때마다 생성할 수도 있지만, @GeneratedValue 어노테이션을 이용해 자동으로 생성할 수도 있습니다.
앞서 말했듯이 해당 어노테이션이 달린 필드를 자동으로 생성하겠다는 뜻입니다. 이 때, parameter인 generater로 어떻게 ID를 생성할지 지정할 수 있습니다. 위 코드의 경우 system-uuid라는 generateor를 사용한다고 명시했죠.
이 때, system-uuid는 @GenericGenerator에 정의된 generator의 이름입니다. @GenericGenerator는 Hibernate가 제공하는 기본 Generator가 아닌 커스텀 generator를 사용하고 싶을 때 이용하죠. 기본 Generator로는 INCREMENTAL, SEQUENCE, IDENTITY 등이 있지만, 우리는 문자열 형태의 uuid를 사용하기 위해 커스텀 Generator를 만들었습니다. uuid를 사용하기 위해 GenericGenerator의 매개변수 strategy로 "uuid"를 넘겼습니다.
@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
List<TodoEntity> findByUserId(String userId);
...
}
우선 알아두어야 할 것은, JpaRepository라는 인터페이스입니다. 이 인터페이스를 사용하기 위해서는 새 인터페이스를 작성해 JpaRepository를 확장(extend)해야 합니다. 이때 JpaReposirotey<T, ID>가 Genetic Type을 받는 것을 주의해야 됩니다. 첫 번째, 매개변수인 T는 테이블에 매핑할 엔티티 클래스이고, ID는 이 엔티티의 기본 키 타입입니다. 위 코드의 경우 TodoEntity와 그 기본 키인 id의 타입인 String을 넣어 주었습니다.
여기서 사용한 @Repository는 위에서 살펴봤던 다른 어노테이션과 유사하게 Component 어노테이션의 특별한 케이스로써 사용됩니다. 따라서 Repository 어노테이션이 달린 인터페이스는 스프링이 자동적으로 관리하게 됩니다.
@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
List<TodoEntity> findByUserId(String userId);
@Query("SELECT t FROM TodoEntity t WHERE t.userId = ?1")
TodoEntity findByUserIdQuery(String userId);
}
복잡한 형태의 쿼리는 @Query 어노테이션을 이용해 지정할 수 있습니다.
구체적인 메서드 이름을 작성하는 방식은 공식 사이트의 레퍼런스를 통해 확인하는 것이 가장 적확하므로 url을 남겨놓겠습니다. 쿼리 메서드 작성 방식
서비스 구현 중에 디버깅을 유용하게 하기 위해서 주로 로그 설정이라는 방식을 많이 사용하죠. 가장 간단한 방법으로는 코드 사이사이에 출력문을 넣어주는 것입니다.
System.out.prinln();
유용하지만 물론 기능이 제한적입니다. 어떤 로그는 그냥 정보를 위한 것이고, 아니면 디버깅을 위한 자세한 정보를 보일 수도 있는데 위의 방법으로는 다양한 경우에 대해 차별성을 가질 수 없죠.
이렇게 용도에 따라 로그를 크게 info, debug, warn,error로 나누고 이를 로그 레벨이라고 부릅니다. 다양한 로그 레벨을 단순하게 출력문으로 구현할 수 있겠지만 이런 기능을 한번에 제공하는 라이브러리가 존재합니다. 바로 Slf4j 라이브러리입니다.
시중에 많은 로그 라이브러리가 나와 있지만 Slf4j는 로그계의 JPA 쯤 됩니다. Slf4j를 구현부에 연결해서 스프링에게 로깅을 맡기겠습니다.
@Slf4j
@Service
public class TodoService {
@Autowired
private TodoRepository repository;
}