본 포스팅은 'React.js, 스프링 부트, AWS로 배우는 웹 개발 101 - 김다정'님의 책을 보고 작성되었습니다.
목차
1. 컨트롤러 레이어: 스프링 REST API 컨트롤러
2. 서비스 레이어: 비지니스 로직
3. 퍼시스턴스 레이어: 스프링 데이터 JPA
4. 정리
컨트롤러 레이어: 스프링 REST API 컨트롤러
- HTTP는 GET/POST/PUT/DELETE/OPTIONS 등과 같은 메서드와 URI를 이용해 서버에 HTTP 요청을 보낼 수 있다.
- 해당 요청을 받은 서버는 어떤 HTTP 메서드를 이용했는지를 알아야 하고 그 후 해당 리소스의 HTTP 메서드에 연결된 메서드를 실행해야 한다.
- 스프링부트의 spring web(라이브러리)의 어노테이션을 이용하면 이 연결을 쉽게할 수 있다.
@RestController
- REST API를 구현하기에 @RestController를 사용해 이 컨트롤러가 RestController임을 명시해준다.
- @RestController를 사용하면 http와 관련된 코드 및 요청과 응답 매핑을 스프링이 알아서 해준다.
package com.example.demo.controller;
import com.example.demo.dto.ResponseDTO;
import com.example.demo.dto.TestRequestBodyDTO;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
//JSON 형태로 객체를 돌려주기 위한 용도로 쓰이는 애너테이션이다.
@RestController
//http://localhost:8080/test로 경로를 매핑 시켜준다
@RequestMapping("test") //리소스
public class TestController {
@GetMapping
public String testController(){
return "Hello world!";
}
}
@GetMapping("/경로 지정")
@GetMapping("/testGetMapping")
public String testControllerWithPath(){
return "Hello world! testGetMapping";
}
- localhost:8080/test/testGetMapping으로 GET요청이 있다면 testControllerWithPath()가 실행된다.
매개변수를 넘겨 받는 방법
@GetMapping("/{id}")
//@PathVariable(required = false) 매개변수가 꼭 필요한 것이 아니라는 의미로 id를 명시하지 않아도 에러가 나지 않는다.
public String testControllerWithPathVariables(@PathVariable(required = false) int id){
return "Hello world! ID " + id;
}
// @PathVariable과 동일하지만 ?id={id}와 같이 요청 매개변수로 넘어오는 값을 변수로 받을 수 있다.
//http://localhost:8080/test/testRequestParam?id=123
@GetMapping("/testRequestParam")
public String testControllerWithRequestParam(@RequestParam(required = false) int id){
return "Hello world! ID " + id;
}
//http://localhost:8080/test/testRequestBody
//Body를 JSON으로 바꿔서 넣어주어야 제대로 확인이 된다.
@GetMapping("/testRequestBody")
//JSON 형태로 HTTPRESPONSE에 담아 반환해주는 애너테이션이다.
public String testControllerRequestBody(@RequestBody TestRequestBodyDTO testRequestBodyDTO) {
return "Hello world! ID " + testRequestBodyDTO.getId() + " Message : " + testRequestBodyDTO.getMessage();
}
- @PathVariable을 사용해 URI 경로로 넘어오는 값을 변수로 받을 수 있다.
- @RequestParam 역시 요청 매개변수로 넘어오는 값을 변수로 받을 수 있다.
- @RequestBody를 사용한 방법은 반환하고자 하는 리소스가 복잡할 때 사용한다.
- 오브젝트처럼 복잡한 자료형을 요청에 보내고 싶을 때 사용한다.
응답으로 오브젝트를 리턴하는 방법
//ResponseDTO를(= 즉, 객체를 반환한다는 뜻이다.) 반환하는 컨트롤러
//http://localhost:8080/test/testResponseBody
@GetMapping("/testResponseBody")
public ResponseDTO<String> testControllerResponseBody(){
List<String> list = new ArrayList<>();
list.add("Hello world! I'm ResponseDTO");
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return response;
}
//엔티티를 반환하는 컨트롤러 메서드(이것도 뭐 객체를 반환하는 것임)
@GetMapping("/testResponseEntity")
public ResponseEntity<?> testControllerResponseEntity(){
List<String> list = new ArrayList<>();
list.add("Hello world! I'm ResponseEntity. And you got 404");
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
//http 상태 코드를 404로 설정해준다.
return ResponseEntity.badRequest().body(response);
}
- @RestController
- @Controller + @ResponseBody
- @Controller
- 스프링이 이 클래스의 오브젝트를 알아서 생성하고 다른 오브젝트들과 의존성을 연결한다는 의미이다.
- @ResponseBody
- 클래스의 메서드가 리턴하는 것은 웹 서비스의 ResponseBody라는 의미이다.
- 다시 말해, 메서드가 리턴할 때 스프링은 리턴된 오브젝트를 JSON 형태로 바꾸고 HttpResponse에 담아 반환한다는 의미이다.
서비스 레이어: 비지니스 로직
- 서비스 레이어는 컨트롤러와 퍼시스턴스 사이에서 비지니스 로직을 수행하는 역할을 한다.
- 서비스 레이어는 HTTP와 긴밀히 연관된 컨트롤러에서 분리되어 있고 또 데이터베이스와 긴밀히 연관된 퍼시스턴스와 분리돼 있다.
- 서비스 레이어는 우리가 개발하고자 하는 로직에 집중할 수 있게 해준다.
Todo서비스 구현
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class TodoService {
public String testService(){
return "Test Service";
}
}
- @Service 애노테이션 안에는 @Component 애노테이션을 가지고 있고 기능 차이는 없다.
- 스프링 컴포넌트이며 기능적으로 비지니스 로직을 수행하는 서비스 레이어임을 알려주는 애노테이션이다.
TodoController
package com.example.demo.controller;
import com.example.demo.dto.ResponseDTO;
import com.example.demo.service.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("todo")
public class TodoController {
//빈을 알아서 찾고 그 빈을 이 인스턴스 멤버 변수로 연결하라는 뜻이다.
@Autowired
private TodoService service;
@GetMapping("/test")
public ResponseEntity<?> testTodo(){
String str = service.testService();//테스트 서비스 사용
List<String> list = new ArrayList<>();
list.add(str);
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return ResponseEntity.ok().body(response);
}
}
퍼시스턴스 레이어: 스프링 데이터 JPA
- 아이템을 데이터베이스에 저장해야한다. 이때 중간 작업이 너무 많아 이를 해결하기 위해서 JPA가 등장했고 JPA는 보다 더 쉽게 자바 코드로 된 엔티티 클래스와 테이블을 매핑해주는 역할 등을 한다.
- ORM은 데이터베이스를 자바 코드(객체지향 언어)등으로 조작할 수 있게 하는 것을 의미한다
엔티티 클래스
- 엔티티 클래스는 클래스 그 자체가 테이블을 정의해야 한다.
- ORM이 엔티티를 보고 어떤 테이블의 어떤 필드에 매핑해야 하는지 알 수 있어야 한다는 뜻이다.
- 기본키, 외래키 구분도 할 수 있어야 한다.
자바 클래스를 엔티티로 정의할 때 주의해야 할 점
- 클래스에는 매개변수가 없는 생성자 NoArgsConstructor가 필요하다.
- Getter/Setter가 필요하다.
- 기본키를 지정해주어야 한다.
엔티티 클래스
package com.example.demo.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
//빌더 패턴 생성을 해주는 애너테이션
@Builder
//매개변수가 없는 생성자를 구현해준다.
@NoArgsConstructor
//클래스의 모든 멤버 변수를 매개변수로 받는 생성자를 구현
@AllArgsConstructor
// 멤버 변수의 setter/ getter 메서드 구현
@Data
//현재 클래스가 엔티티 클래스임을 명시해준다.
@Entity
//테이블 이름 지정
@Table(name = "Todo")
public class TodoEntity {
//기본키 지정
@Id
// ID를 자동생성하겠다는 의미이다. generator로 어떤 방식으로 ID를 자동생성할지 지정
@GeneratedValue(generator = "system-uuid")
//하이버네트가 제공하는 하는 것이 아닌 나만의 generator를 사용하고 싶을 때 사용한다.
@GenericGenerator(name = "system-uuid", strategy = "uuid")
private String id; //이 오브젝트의 아이디
private String userId; // 이 오브젝트를 생성한 사용자의 아이디
private String title;
private boolean done; // todo를 완료한 경우를 위한 멤버 변수
}
- @Builder
- 빌더 패턴 생성을 해주는 애너테이션
- @NoArgsConstructor
- 매개변수가 없는 생성자를 구현해준다.
- @AllArgsConstructor
- 클래스의 모든 멤버 변수를 매개변수로 받는 생성자를 구현
- @Data
- 멤버 변수의 setter/ getter 메서드 구현
- @Entity
- 현재 클래스가 엔티티 클래스임을 명시해준다.
- @Table(name = "Todo")
- 테이블 이름 지정
- @Id
- 기본키 지정
- @GeneratedValue(generator = "system-uuid")
- ID를 자동생성하겠다는 의미이다. generator로 어떤 방식으로 ID를 자동생성할지 지정
- @GenericGenerator(name = "system-uuid", strategy = "uuid")
- 하이버네트가 제공하는 하는 것이 아닌 나만의 generator를 사용하고 싶을 때 사용한다.
이제 JPA가 Entity 클래스와 데이터베이스의 테이블을 매칭하여 매핑해준다.
TodoRepository
package com.example.demo.persistence;
import com.example.demo.model.TodoEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
}
- JpaRepository 인터페이스를 사용하기 위해서는 새 인터페이스를 작성해 JpaRepository 를 확장해야 한다.
- 이때 JpaRepository는 제네릭 타임을 받는 것을 주의하자!
- @Repository 애노테이션도 @Component 애노테이션의 특별 케이스로 해당 클래스를 루트 컨테이너에 빈(Bean) 객체로 생성해주는 애노테이션이다.
JpaRepository 인터페이스
/*
* Copyright 2008-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository;
import java.util.List;
import javax.persistence.EntityManager;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;
/**
* JPA specific extension of {@link org.springframework.data.repository.Repository}.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
* @author Sander Krabbenborg
* @author Jesse Wouters
* @author Greg Turnquist
*/
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#findAll()
*/
@Override
List<T> findAll();
/*
* (non-Javadoc)
* @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
*/
@Override
List<T> findAll(Sort sort);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable)
*/
@Override
List<T> findAllById(Iterable<ID> ids);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable)
*/
@Override
<S extends T> List<S> saveAll(Iterable<S> entities);
/**
* Flushes all pending changes to the database.
*/
void flush();
/**
* Saves an entity and flushes changes instantly.
*
* @param entity entity to be saved. Must not be {@literal null}.
* @return the saved entity
*/
<S extends T> S saveAndFlush(S entity);
/**
* Saves all entities and flushes changes instantly.
*
* @param entities entities to be saved. Must not be {@literal null}.
* @return the saved entities
* @since 2.5
*/
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
/**
* Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
* first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
* method.
*
* @param entities entities to be deleted. Must not be {@literal null}.
* @deprecated Use {@link #deleteAllInBatch(Iterable)} instead.
*/
@Deprecated
default void deleteInBatch(Iterable<T> entities) {
deleteAllInBatch(entities);
}
/**
* Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
* first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
* method.
*
* @param entities entities to be deleted. Must not be {@literal null}.
* @since 2.5
*/
void deleteAllInBatch(Iterable<T> entities);
/**
* Deletes the entities identified by the given ids using a single query. This kind of operation leaves JPAs first
* level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this method.
*
* @param ids the ids of the entities to be deleted. Must not be {@literal null}.
* @since 2.5
*/
void deleteAllByIdInBatch(Iterable<ID> ids);
/**
* Deletes all entities in a batch call.
*/
void deleteAllInBatch();
/**
* Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
* implemented this is very likely to always return an instance and throw an
* {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
* immediately.
*
* @param id must not be {@literal null}.
* @return a reference to the entity with the given identifier.
* @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
* @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
*/
@Deprecated
T getOne(ID id);
/**
* Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
* implemented this is very likely to always return an instance and throw an
* {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
* immediately.
*
* @param id must not be {@literal null}.
* @return a reference to the entity with the given identifier.
* @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
* @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
* @since 2.5
*/
@Deprecated
T getById(ID id);
/**
* Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
* implemented this is very likely to always return an instance and throw an
* {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
* immediately.
*
* @param id must not be {@literal null}.
* @return a reference to the entity with the given identifier.
* @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
* @since 2.7
*/
T getReferenceById(ID id);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
*/
@Override
<S extends T> List<S> findAll(Example<S> example);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
*/
@Override
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
- JpaRepository의 첫 번째 매개변수 T는 테이블에 매핑될 엔티티 클래스를 넣어주어야 한다.
- JpaRepository의 두번째 매개변수 ID는 이 엔티티의 기본 키 타입을 넣어주어야 한다. (우리 엔티티의 경우 String이어서 String이 들어간다.)
@Service와 @Repository의 차이?
- 컨트롤러 : @Controller (프레젠테이션 레이어, 웹 요청과 응답을 처리함)
- 로직 처리 : @Service (서비스 레이어, 내부에서 자바 로직을 처리함)
- 외부I/O 처리 : @Repository (퍼시스턴스 레이어, DB나 파일같은 외부 I/O 작업을 처리함)
출처: https://codevang.tistory.com/258
TodoService에서 TodoRepository 사용
package com.example.demo.service;
import com.example.demo.model.TodoEntity;
import com.example.demo.persistence.TodoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TodoService {
@Autowired
private TodoRepository repository;
public String testService(){
//TodoEntity 생성
TodoEntity entity = new TodoEntity().builder().title("My first todo item").build();
//TodoEntity 저장
repository.save(entity);
//TodoEntity 검색
TodoEntity savedEntity = repository.findById(entity.getId()).get();
return savedEntity.getTitle();
}
}
[어떻게 위와 같은 결과가 나온 것일까?]
- JpaRepository는 기본적인 데이터베이스 오퍼레이션 인터페이스를 제공한다.
- 이는 JPA 관련 서적이나 관련 강의 등을 보고 좀 알아두면 좋다!!!
- save, findById, findAll 등이 기본적으로 제공되는 인터페이스에 해당한다.
- 구현은 스프링 데이터 JPA가 실행 시에 알아서 해준다.
- 또한 JpaRepository는 구현 클래스 없이도 사용할 수 있는데 이 이유는 스프링이 methodInterceptor이라는 AOP 인터페이스를 사용해 가능한 일이라고 한다.
- 또한 스프링은 우리가 JpaRepository를 사용하려 부르면 이 call을 가로채 본인이 쿼리를 작성해준다고 한다. (우리가 직접 쿼리문을 작성할 일이 없다.)
정리
- 퍼시스턴스 레이어
- 데이터베이스와 통신하며 필요한 쿼리를 보내고 해석해 엔티티 오브젝트로 변환해 주는 역할
- 서비스 레이어
- HTTP나 데이터베이스 가은 외부 컴포넌트로부터 추상화돼 우리가 온전히 비지니스 로직에만 집중할 수 있게 해주는 역할
- 컨트롤러 레이어
- 주로 HTTP 요청과 응답을 어떻게 넘겨 받고 리턴하는냐(응답), 즉 외부 세계와 통신하는 규약을 정의하는 역할