dependencies에 spring web, spring data jpa, h2 database, lombok을 넣어주고 generate 한다.

압축파일은 java workspace에 압축 풀어준다.

File - Import - Gradle - Existing Gradle Project - Next - Project Root Directory 설정 - Override workspace setting 체크, Gradle wrapper 선택 - Finish
프로젝트 우클릭 후 Run as - Spring Boot App
Preferences - Server - Runtime Environments에서 Tomcat 10을 Add
프로젝트 우클릭 후 Properties - Projcet Facets - Dynamic Web Module 체크

프로젝트 우클릭 후 Run as - Run on server
웹브라우저에서 http://localhost:8080 에 접속한다.

gradle에 sourceCompatibility = '17'와 implementation 'org.springframework.boot:spring-boot-starter'이 있는지 확인한다.
나의 경우에는 없어서 에러가 떴다.
자세히 보니 구조가 이상하다
이클립스 마켓플레이스에서 STS4를 설치하자
그리고 프로젝트를 다시 생성해 보자
스프링 부트 프로젝트를 하다 보면 라이브러리를 추가하는 상황이 온다. 메이븐 리포지터리를 이용해 라이브러리를 추가하면 된다.
MVN Repository에서 Guava를 검색 후 Usages가 많은 것을 선택하자.
30.1.1-jre를 선택할 것이다.
구글 guava 라이브러리를 이용하면 자바 Collection을 간편하게 활용할 수 있다.
이클립스에 lombok 라이브러리도 추가하면 좋다.
롬복은 getter, setter, builder, constructor를 자동으로 작성해 준다.
코드의 양을 줄이고 개발 시간을 단축할 수 있다.
롬복 jar 파일을 다운로드해서 롬복 jar 파일이 있는 해당 경로로 cmd 창을 열어준다.
java -jar lombok.jar
아마 자동으로 이클립스를 찾아줄 텐데, 찾지 못한다면 수동으로 이클립스 설치 폴더를 선택한 뒤 install/update 한다.
이클립스 재시작 후 Help - About Eclipse IDE를 선택하면 롬복이 설치된 것을 확인할 수 있다.

프로젝트 우클릭 - Java Compiler - Annotation Processing - Apply and Close

롬복의 동작 여부를 확인하기 위해 TodoModel.java를 생성한다.
import lombok.Builder;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@Builder
@RequiredArgsConstructor
public class TodoModel {
@NonNull
private String id;
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping
public String testController() {
return "Hello World!";
}
}
프로젝트 실행
웹 브라우저에서 localhost:8080/test를 입력하여 확인하기

포스트맨은 백엔드 개발을 할 때 request와 response를 확인할 수 있는 프로그램이다.
https://www.postman.com/downloads/
포스트맨을 통해 GET request를 실행한다.

@GetMapping("/{id}")
public String testControllerWithPathVariables(@PathVariable(required = false) int id) {
return "Hello World! ID " + id;
}
Name for argument of type [int] not specified, and parameter name information not available via reflection. Ensure that the compiler uses the '-parameters' flag.
참고 - 스프링 부트 3.2 매개변수 이름 인식 문제
참고 - 인프런 @PathVariable 변수명 같을때 생략시 오류
PathVariable에 변수명을 명시해 주는 것이 권장하는 방법이다.
@GetMapping("/{id}")
public String testControllerWithPathVariables(@PathVariable("id") int id) {
return "Hello World! ID " + id;
}
@RequestParam을 이용하여 매개변수를 전달 받을 수 있다.
이때 request 요청 URI는 localhost:8080/test/Param?id=123 형태다.
@GetMapping("/Param")
public String testControllerRequestParam(@RequestParam("id") int id) {
return "Hello World! ID param " + id;
}

클라이언트 요청을 JSON 형식으로 받아 스트링을 반환하도록 해본다
package com.example.todo.dto;
import lombok.Data;
@Data
public class TestRequestBodyDTO {
private int id;
private String message;
}
@GetMapping("/testRequestBody")
public String testControllerRequstBody(@RequestBody TestRequestBodyDTO testRequestBodyDTO) {
return "Hello World! ID: " + testRequestBodyDTO.getId() + " / Message: " + testRequestBodyDTO.getMessage();
}

@RestController 내부는 @Controller, @ResponseBody 어노테이션으로 구성된다.
@ResponseBody는 응답 객체를 만들어 클라이언트에게 반환한다.
package com.example.todo.dto;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class ResponseDTO<T> {
private String error;
private List<T> data;
}
@GetMapping("/testResponseBody")
public ResponseDTO<String> testControllerResponseBody() {
List<String> list = new ArrayList<>();
list.add("Hello world! I'm ResponseDTO");
list.add("See you!");
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return response;
}

@GetMapping("/testResponseEntityOk")
public ResponseEntity<?> testControllerResponseEntityOk() {
List<String> list = new ArrayList<>();
list.add("Hello world! I'm ResponseEntity. And you get 200!");
list.add("See you");
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return ResponseEntity.ok().body(response);
}
@GetMapping("/testResponseEntityBad")
public ResponseEntity<?> testControllerResponseEntityBad() {
List<String> list = new ArrayList<>();
list.add("Hello world! I'm ResponseEntity. And you get 400!");
list.add("See you");
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return ResponseEntity.badRequest().body(response);
}


레이어드 아키텍처
1. 프리젠테이션 레이어(Controller) - dto
2. 비즈니스 레이어(Service) - model
3. 퍼시스턴스 레이어(Persistence) - entity
4. 데이터베이스 레이어(DB)
@Service는 스테레오 타입 컴포넌트다.
import org.springframework.stereotype.Service;
@Service
public class TodoService {
public String testService() {
return "Test Service";
}
}
@Autowired
private TodoService todoService;
@GetMapping("/service")
public ResponseEntity<?> testService() {
String str = todoService.testService();
List<String> list = new ArrayList<>();
list.add(str);
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return ResponseEntity.ok().body(response);
}
스프링 부트로 웹 개발 시 패키지에 포함되는 일반적인 클래스는 크게 3가지다.
package com.example.todo.model;
import org.hibernate.annotations.GenericGenerator;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "Todo")
public class TodoEntity {
@Id
@GeneratedValue(generator = "system-uuid") // 자동으로 id 생성
@GenericGenerator(name = "system-uuid", strategy = "uuid")
private String id;
private String usrId;
private String title;
private boolean done;
}
package com.example.todo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.todo.model.TodoEntity;
@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
}
@Service
public class TodoService {
@Autowired
private TodoRepository repository;
public String testService() {
// Todo entity 생성
TodoEntity entity = TodoEntity.builder().title("my first todo item").build();
// Todo entity 저장
repository.save(entity);
// Todo entity 검색
TodoEntity savedEntity = repository.findById(entity.getId()).get();
return savedEntity.getTitle();
}
}
@GetMapping("/repository")
public ResponseEntity<?> testRepository() {
String str = todoService.testService();
List<String> list = new ArrayList<>();
list.add(str);
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return ResponseEntity.ok().body(response);
}

@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
List<TodoEntity> findByUsrId(String usrId);
}
public String testService() {
// Todo entity 생성
TodoEntity entity = TodoEntity.builder().usrId("usrId01").title("my first todo item").build();
// Todo entity 저장
repository.save(entity);
// Todo entity 검색
TodoEntity savedEntity = repository.findByUsrId(entity.getUsrId()).get(0);
return savedEntity.getUsrId();
}

JPA에서 제공하는 기본 쿼리 외에 SQL을 사용하여 사용자가 쿼리를 지정할 수 있다.
@NamedQuery를 사용한 쿼리는 Entity 클래스에서 정의하여 사용한다.
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "Todo")
@NamedQuery(name = "TodoRepository.searchByUsrId", query = "select t from TodoEntity t where t.usrId = ?1")
public class TodoEntity {
@Id
@GeneratedValue(generator = "system-uuid") // 자동으로 id 생성
@GenericGenerator(name = "system-uuid", strategy = "uuid")
private String id;
private String usrId;
private String title;
private boolean done;
}
@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
List<TodoEntity> searchByUsrId(String usrId);
}
public String testService() {
// Todo entity 생성
TodoEntity entity = TodoEntity.builder().usrId("usrId01").title("my first todo item").build();
// Todo entity 저장
repository.save(entity);
// Todo entity 검색
TodoEntity savedEntity = repository.searchByUsrId(entity.getUsrId()).get(0);
return savedEntity.getUsrId();
}

다음은 @Query를 사용하여 SQL을 질의하는 방법이다.
TodoRepository.java만 수정하면 되므로 비교적 간단하다.
@NamedQuery 구문은 삭제한다.
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "Todo")
public class TodoEntity {
@Id
@GeneratedValue(generator = "system-uuid") // 자동으로 id 생성
@GenericGenerator(name = "system-uuid", strategy = "uuid")
private String id;
private String usrId;
private String title;
private boolean done;
}
@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
@Query("select t from TodoEntity t where t.usrId = ?1")
List<TodoEntity> searchByUsrId(String usrId);
}

@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "Todo")
public class TodoEntity {
@Id
@GeneratedValue(generator = "system-uuid") // 자동으로 id 생성
@GenericGenerator(name = "system-uuid", strategy = "uuid")
private String id;
private String usrId;
private String title;
private boolean done;
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoDTO {
private String id;
private String title;
private boolean done;
// entity to dto
public TodoDTO(final TodoEntity entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.done = entity.isDone();
}
// dto to entity
public static TodoEntity toEntity(final TodoDTO dto) {
return TodoEntity.builder()
.id(dto.getId())
.title(dto.getTitle())
.done(dto.isDone())
.build();
}
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class ResponseDTO<T> {
private String error;
private List<T> data;
}
@Slf4j
@Service
public class TodoService {
@Autowired
private TodoRepository repository;
public Optional<TodoEntity> create(final TodoEntity entity) {
// Validation
validate(entity);
repository.save(entity);
return repository.findById(entity.getId());
}
public void validate(final TodoEntity entity) {
if (entity == null) {
log.warn("Entity cannot be null");
throw new RuntimeException("Entity cannot be null");
}
if (entity.getUsrId() == null) {
log.warn("Unknown user");
throw new RuntimeException("Unknown user");
}
}
}
@Slf4j
@RestController
@RequestMapping("/todo")
public class TodoController {
@Autowired
private TodoService service;
@PostMapping
public ResponseEntity<?> createTodo(@RequestBody TodoDTO dto) {
try {
log.info("Log: createTodo entrance");
TodoEntity entity = TodoDTO.toEntity(dto);
log.info("Log:dto -> entity ok");
entity.setUsrId("temporary-usrId");
Optional<TodoEntity> entities = service.create(entity);
log.info("Log:service.create ok");
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
log.info("Log:entities -> dtos ok");
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
return ResponseEntity.ok().body(response);
} catch (Exception e) {
String error = e.getMessage();
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
return ResponseEntity.badRequest().body(response);
}
}
}

@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
@Query("select t from TodoEntity t where t.usrId = ?1")
List<TodoEntity> findByUsrId(String usrId);
}
public List<TodoEntity> retrieve(final String usrId) {
return repository.findByUsrId(usrId);
}
@GetMapping
public ResponseEntity<?> retrieveTodoList() {
String temporaryUsrId = "temporary-usrId";
List<TodoEntity> entities = service.retrieve(temporaryUsrId);
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
return ResponseEntity.ok().body(response);
}



application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false
public Optional<TodoEntity> update(final TodoEntity entity){
validate(entity);
if (repository.existsById(entity.getId())) {
repository.save(entity);
} else {
throw new RuntimeException("Unknown id");
}
return repository.findById(entity.getId());
}
public Optional<TodoEntity> updateTodo(final TodoEntity entity) {
validate(entity);
final Optional<TodoEntity> original = repository.findById(entity.getId());
original.ifPresent(todo -> {
todo.setTitle(entity.getTitle());
todo.setDone(entity.isDone());
repository.save(todo);
});
return repository.findById(entity.getId());
}
@GetMapping("/update")
public ResponseEntity<?> update(@RequestBody TodoDTO dto) {
try {
TodoEntity entity = TodoDTO.toEntity(dto);
entity.setUsrId("temporary-usrId");
Optional<TodoEntity> entities = service.update(entity);
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
return ResponseEntity.ok().body(response);
} catch (Exception e) {
String error = e.getMessage();
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
return ResponseEntity.badRequest().body(response);
}
}
@PutMapping
public ResponseEntity<?> updateTodo(@RequestBody TodoDTO dto) {
try {
TodoEntity entity = TodoDTO.toEntity(dto);
entity.setUsrId("temporary-usrId");
Optional<TodoEntity> entities = service.updateTodo(entity);
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
return ResponseEntity.ok().body(response);
} catch (Exception e) {
String error = e.getMessage();
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
return ResponseEntity.badRequest().body(response);
}
}



http://localhost:8080/h2-console에 application.properties에 작성한 접속 정보로 접속 시 DB를 확인할 수 있다.
