1. 프로젝트 환경설정
가. 프로젝트 설정
1) 기본 설정
•
start.spring.io 프로젝트 기본 설정
→ Project는 Gradle 선택
•
build tool(gradle)은 위의 두 가지 의존성이 의존하고 있는 여러 의존성에 대해 자동으로 가져옴
→ build tool에 의해 가져온 의존성들은 build.gradle에 나열되어 있음
2) 주요 라이브러리
•
스프링부트 라이브러리
→ spring-boot-starter-web
→ spring-boot-starter-tomcat: 톰캣 (웹서버)
→ spring-webmvc: 스프링 웹 MVC
→ spring-boot-starter-thymeleaf: 타임리프 템플릿 엔진(View)
→ spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅
→ spring-boot: spring-core
→ spring-boot-starter-logging: logback, slf4j
•
테스트 라이브러리: spring-boot-starter-test
→ junit: 테스트 프레임워크
→ mockito: 목 라이브러리
→ assertj: 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
→ spring-test: 스프링 통합 테스트 지원
3) logging
•
현업에서 실 서버에서 작업할 때, system.out.println을 사용하지 않고 log로 확인한다
•
관련 종속성은 'slf4j'
•
스프링부트에서 'logback'으로 가져와서 표준으로 사용함
4) 필요한 기능 찾아 쓰기
•
스프링 자체가 자바의 대부분의 기능을 구현하는 만큼 그 기능이 방대함
•
필요한 기능이 있다면 공식 사이트에서 검색을 통해 찾아서 사용해야 함
•
이하 Reference Document의 Spring Boot Features를 중점으로 관련 키워드를 검색하여 필요 기능을 찾는다.
5) Using Muliple Schema or Catalog
•
MySQL에서 Table의 상위 개념을 Schema로 정의하지만 SpringBoot에서 해당 개념을 Catalog라는 개념으로 맵핑한다. 따라서 Entity에 대해 맵핑된 테이블을 Default Schema가 아니라 다른 Schema에서 찾을 경우 @Table의 Catalog 옵션을 활용해야 한다.
→ RDBMS의 계층 구조는 다음과 같은 4계층 구조다. 인스턴스 <- 데이터베이스 <- 스키마 <- 테이블
→ MySQL의 경우 데이터베이스와 스키마를 합쳐서 3계층 구조로 구현되었다.
Entity
@Table(name = "om_log_image_view", catalog = "bghd_log")
public class OmImageViewLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
Java
복사
5) Setting Multiple DataSources
참고) properties or yaml
참고) Database Config
•
각각의 datasource에 대한 환경변수 주입
•
주입한 환경변수를 DataSourceProperties에 생성하여 연관 빈 생성
•
DataSourceProperties를 바탕으로 DataSource 빈 생성
•
Entity Manager 설정
→ Data Source 맵핑
→ Entity가 정의된 package 맵핑
ex) 위의 Database Config 코드 참고, entityManagerFactoryBean.setPackagesToScan("me.om.omcommon.domain")
or entityManagerFactoryBean.setPackagesToScan(new String[]{"mj.api.domain", "mj.core.domain"})
•
Transactional Manager 설정
→ 하나의 트랜잭션으로 묶여서 처리할 엔티티매니저를 등록
•
ChainedTransactionManager 설정
→ 두 개 이상의 트랜잭션 매니저가 설정된 경우, 두 개의 트랜잭션 매니저를 연결한 ChainedTransactionManager 설정하여 Transaction 적용해야 함
// DatabaseConfig.java
@Bean(name = "chainedTransactionManager")
public ChainedTransactionManager getChainedTransactionManager(
@Qualifier("logTransactionManager") PlatformTransactionManager logTm,
@Qualifier("transactionManager") PlatformTransactionManager tm) {
return new ChainedTransactionManager(logTm, tm);
}
// LogService.java
@Transactional(value = "chainedTransactionManager")
public void aggregateDownloadLogBeforeNowAndAfter(final long days) {
List<Long> productIds = readLogsBeforeNowAndAfter(days);
for (Long productId : productIds) {
Optional<OmImageDownloadCount> downloadCount = loadDownloadCountWith(productId);
int countUpdating = countBy(productId);
if (downloadCount.isPresent()) {
updateOldDownloadCountLog(downloadCount.get(), countUpdating);
continue;
}
createNewDownloadCountLog(productId, countUpdating);
}
}
Java
복사
나. thymeleaf 동작 흐름
•
Client(web browser)에서 Get 방식으로 요청(localhost:8080/hello)
•
내장 톰켓 서버가 해당 요청에 대해 helloController::hello로 연결
→ @GetMapping("hello") 어토테이션에 따라 위와 같이 스프링부트가 연결할 수 있음
•
helloController는 스프링부터로부터 전달받은 매개변수 모델에 대해 데이터 처리
→ data라는 변수에 "hello!!!" 문자열 할당
•
viewResolver는 웹 브라우저 요청에 대한 응답으로 helloController의 반환값으로 관련 템플릿을 찾아서 전달함(resources:templates/hello.html)
→ 스프링 부트 템플릿엔진 기본 viewName 매핑
resources:templates/ +{ViewName}+ .html
다. Gradle
1) Gradle vs Maven
최근 빌드 자동화툴은 Java와의 호환성이 좋은 Grooby 기반의 Gradle을 주로 사용
•
Gradle은 Grooby 기반 빌드 자동화툴
•
Maven은 XML 기반 빌드 자동화툴
•
Grooby는 JVM 위에서 동작하는 스크립트 언어로, Java 문법과 유사하여 Java와의 호환성이 좋음
2) Gradle 실행
•
실행명령: gradle init
실행 결과
3) Gradle 설정
•
settings.gradle: 프로젝트 수준의 빌드 설정
•
build.gradle: 어플리케이션 수준의 빌드 설정
4) Gradle Build
•
./gradlew run
라. 빌드 & 실행
1) 빌드
•
프로젝트 폴더(hello-spring)에서 이하 gralew 파일 빌드하기
•
./gradlew build
•
빌드 성공 시 /build/libs 이하의 스냅샷이 생성됨
•
스냅샷으로 프로그램 실행
→ cd /build/libs
→ java -jar hello-spring-0.0.1-SNAPSHOT.jar
→ .jar 파일은 스냅샷이므로 libs 이하에 생성된 파일명 확인할 것
•
빌드 실패 원인 해결
→ 에러 메시지: Please check that /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home contains a valid JDK installation.
→ 자바 파일에 대해 빌드를 수행하는 tools.jar가 해당 경로에 없기 때문에 발생함
→이하 스택오버플로우의 가장 많은 지지를 얻은 답변 참고하여 해결https://stackoverflow.com/questions/64968851/could-not-find-tools-jar-please-check-that-library-internet-plug-ins-javaapple
→ .zshrc의 경우 default로 제공이 안되므로 홈디렉토리에서 해당 파일을 생성해서 경로를 재설정해야 함
→ vim ~/.zshrc
2) 환경별 설정값 주입
•
(Community Version) IntelliJ IDE에 주입
→ Run → Edit Configurations
→ 환경변수(E) 부분에 SPRING_PROFILES_ACTIVE 부분에 입력
→ 스크린샷의 경우 application-test.properties 설정값 주입
•
(Ultimate version) IntelliJ IDE 주입
→ Run → Edit Configurations
→ Active profiles에 로컬 환경값 주입(apllication-dev.yml 설정값 주입)
2. 스프링 웹 개발 기초
가. 웹 개발 방식
•
정적 컨텐츠
→ 이미 작성된 정적 컨텐츠를 서버에서 가공하지 않고 그대로 클라이언테에게 전달
→ 정적으로 HTML 전달
•
MVC와 템플릿 엔진
→ HTML의 내부를 서버에서 가공하여 클라이언트에게 전달
→ 동적으로 HTML 전달
•
API
→ JSON 방식으로 전달하여 클라이언트 사이드에서 작업이 이루어지도록
→ 리액트, 뷰가 API 방식으로
→ 서버에서 서버로 통신할 때, HTML이 필요 없으므로 API 방식 이용
→ JSON 형식의 데이터 전달
나. 정적 컨텐츠 동작 흐름
•
정적 컨텐츠 동작 흐름
◦
◦
톰캣서버가 해당 요청을 스프링 컨테이너 내부의 Controller에게 전달함
→ 요청처리 우선순위는 Controller에게 있음
◦
Controller에 hello-static.html 처리에 대한 책임이 없음 확인
◦
톰켓 서버는 resources/static 내 관련 데이터가 있는 지 확인
◦
내부에 관련 데이터 존재함에 따라 해당 .html을 웹 브라우저에 응답으로 전송함
다. MVC 패턴
1) MVC 패턴 기초
•
MVC(Model View Controller): 사용자 인터페이스에서 비지니스로직을 분리한 디자인 패턴
→ Model: 어플리케이션의 정보(데이터), 어떠한 동작을 수행하는 코드
→ View: 사용자 인터페이스 요소, 모델로부터 값을 가져와서 사용자에게 보여줌
→ Controller: 데이터와 비지니스 로직 간 상호동작 관리, 모델의 상태를 바꿀 때 사용함
→ 장점: 어플리케이션의 유지보수성, 확장성, 유연성이 증가
•
MVC 간 상호작용
→ 사용자는 컨트롤러를 사용하여 모델의 상태 변경
→ 모델은 최신의 상태를 뷰에게 전달
→ 뷰는 최신의 상태를 사용자에게 보여줌
•
과거 jsp로 개발하던 시절에는 MVC로 나뉘지 않고 통합
→사용자 인터페이서에 비지니스 로직과 시각적 요소가 혼재
→ 책임 역할 분배 X
// controller
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam("name") String name, Model model) {
model.addAttribute("name", "helloMVC!!!!!");
return "hello-mvc-mvc";
}
Java
복사
// hello-mvc-mvc.html
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="'hello ' + ${name}">hello! empty</p>
</body>
</html>
HTML
복사
2) MVC 동작 흐름
•
•
웹서버는 해당 요청이 컨트롤러에 책임이 있는 지 확인
•
컨트롤러의 책임이라면 Mapping된 메소드를 실행하고 템플릿 엔진이 처리할 html의 이름을 반환함
•
ViewResolver는 컨트롤러가 반환한 문자열을 바탕으로 관련 템플릿파일(.html 파일)을 찾아서 템플릿 엔진에 전달함
•
해당 템플릿 엔진은 .html 파일을 변환하여 웹 브라우저에 전송함
→ 정적 컨텐츠의 경우 변환과정이 없음
라. HTTP Request 처리 흐름
1) Controller 중심 HTTP 요청 및 응답 처리
•
웹브라우저의 요청을 웹 서버가 받아서 스프링 컨테이너 내 helloController에게 전달
•
helloController는 hello-api와 맵핑된 메소드에 해당 요청에 대한 처리책임을 넘김
•
hello-api와 맵핑된 메소드는 관련 연산을 수행하고 값을 반환함
•
위의 메소드에 @ResponseBody 어노테이션이 붙어있다면 HttpMessageConverter가 맡아서 처리함
→ 위의 @ResponseBody와 더불어 HTTP Accept Header의 정보, 둘의 합쳐서 HttpMessageConverter가 해당 요청을 처리할 지 결정됨.
→ MVC 중 V(View) 방식으로 출력할 경우, HttpMessageConverter가 아니라 viewResolver가 해당 역할을 맡음
•
위의 메소드의 반환값이 객체이면 HttpMessageConverter 내부의 JsonConverter가 맡아서 웹 브라우저에 응답함
→ 반면, 반환값이 문자열이라면 HttpMessageConverter 내부의 StringConverter가 맡아서 처리함
2) Spring MVC Request Life Cycle
스프링 어플리케이션은 POJO 클래스와 설정 메타정보를 이용해 IoC 컨테이너가 만들어주는 오브젝트의 조합 - 토비의 스프링 3
•
웹에서 스프링 어플리케이션의 동작 방식
◦
클라이언트로부터 요청이 발생하면 서블릿 컨테이너는 해당 요청을 받아서 서블릿을 동작시킨다
◦
서블릿은 미리 생성한 WebApplicationContext를 통해 Bean을 호출한다
image from p782, 이일민, 토비의 스프링 3
•
Dispatcher Servlet
◦
역할: Request의 URL 패턴에 따라 사전 mapping된 Controller로 요청을 전달함. 더불어 Spring IoC container와 통합되어 있어 스프링의 여러 기능을 사용하는데 여러모로 기여함
→ Dispatcher Servlet은 ‘Front Controller’ Design Pattern의 구현체로 볼 수 있음
→ Dispatcher Servlet은 서블릿의 일종으로, 설정 메타정보를 참고하여 ApplicationContext를 생성한다. 요청을 처음으로 받아서 적절한 Bean(or Object or Controller)을 찾아서 실행시킴
◦
Front Controller의 요청 처리 흐름도
image from 17.2 The DispatcherServlet, https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/mvc.html
•
Filter
•
Interceptor
역할: 특정 Controller로 전달되는 HTTP Request에 대한 전처리
주요작업: 인증 및 인가(Authorization, Authentication), Claim Token Parsing 등
조작 대상: HttpServletRequest, HttpServletResponse로 전달 받는 객체 내 데이터 조작 가능
→ Filter와 달리 전달 받는 객체 자체에 대해 조작 불가
•
Filter vs Interceptor
◦
filter: 웹서버(톰캣)를 거친 후 SpringMVC 집입 전
◦
interceptor: Spring MVC 내 DispatcherServlet을 거친 후 Controller 진입 전
3. Validation
가. Bean Validation
•
‘Bean Validation’은 자바 생태계에서 유효성 검사의 표준에 가까운 기술임
→ Spring과 Spring Boot에서도 ‘Bean Validation’ 활용하여 유효성 검사할 것을 권장
나. Controller Validation
1) Valid
•
메소드의 매개변수 또는 필드 수준에서의 유효성 검사에 사용됨
•
유효하지 않은 데이터 입력 시 MethodArgumentNotValidException 발생
→ Controller에서 해당 예외를 전달 받으면 HTTP status 400 Bad Request 로 응답 메시지 전송
@RestController
class ValidateRequestBodyController {
@PostMapping("/validateBody")
ResponseEntity<String> validateBody(@Valid @RequestBody Input input) {
return ResponseEntity.ok("valid");
}
}
Java
복사
code from https://reflectoring.io/bean-validation-with-spring-boot/
class Input {
@Min(1)
@Max(10)
private int numberBetweenOneAndTen;
@Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")
private String ipAddress;
// ...
}
Java
복사
code from https://reflectoring.io/bean-validation-with-spring-boot/
2) Validated
•
클래스 수준의 유효성 검사에 사용됨
@RestController
@Validated
class ValidateParametersController {
@GetMapping("/validatePathVariable/{id}")
ResponseEntity<String> validatePathVariable(
@PathVariable("id") @Min(5) int id) {
return ResponseEntity.ok("valid");
}
@GetMapping("/validateRequestParameter")
ResponseEntity<String> validateRequestParameter(
@RequestParam("param") @Min(5) int param) {
return ResponseEntity.ok("valid");
}
}
Java
복사
code from https://reflectoring.io/bean-validation-with-spring-boot/
•
유효하지 않은 데이터 입력 시 ConstraintViolationException 발생
→ MethodArgumentNotValidException과 달리 해당 예외는 자동 응답 메시지 전송하지 않음
→ HTTP status 400 Bad Request 응답 의도할 경우, 아래와 같이 커스텀해서 관련 기능을 정의해야 함
@RestController
@Validated
class ValidateParametersController {
// request mapping method omitted
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
return new ResponseEntity<>("not valid due to validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);
}
}
Java
복사
다. Entity Validation
4. DB 접근 기술
가. DB Connection
1) H2
•
→ chmod 755 h2.sh
•
실행
→ ./h2.sh
•
home directory에 test.mv.db 생성 확인
•
이후 JDBC URL 부분에 jdbc:h2:tcp://localhost/~/test로 변경하여 접속
→ 기존 URL로 접속하면 IDE 콘솔과 웹 어플리케이션에서 DB에 동시접속이 안됨
•
만약 H2 설치과정 중 에러가 발생하면 test.mv.db 파일 삭제 후 재설치
2) MySQL
나. JDBC DB 접근 기술
1) 설정 정보 추가
•
종속성
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
Java
복사
•
DB 설정 정보
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
Java
복사
2) JDBC Template 특징
•
순수 JDBC와 환경설정은 동일함
•
JDBC Template의 장점은 순수 JDBC에서 발생하는 반복 코드를 제거한다는 것
•
SQL은 순수 JDBC와 같이 직접 작성해야 함
•
실무에서 많이 사용하고 있음
다. JPA DB 접근 기술
•
이하 참고
라. Initializing DB using SQL Script
1) 설정
•
MySQL 등 Disk 기반 DB의 경우 추가적인 설정이 필요함
→ Script 파일 기반 DB 초기화는 In-Memory DB의 경우에만 기본값으로 지정됨
→ application.properties에 spring.sql.init.mode=always 설정을 추가하여 DB 유형에 관계없이 SQL Script 기반 초기화되도록 작업 필요
2) SQL Script 작성
5. Test
가. 스프링 통합 테스트
1) 유용한 어노테이션
•
@SpringBootTest: 스프링 컨네이너와 테스트를 함께 실행함
→ 스프링을 동작시켜서 통합적으로 관련 기능을 테스트할 때 사용함
→ 하지만 순수 자바 코드만으로 기능의 단일성을 테스트하는 테스트 코드를 권장함
•
@Transactional: 테스트 시작 전에 트랜잭션을 시작하고 테스트 케이스가 끝나면 롤백함
→ 테스트가 끝나면 테스트에 사용된 데이터라 자동으로 제거되므로 테스트의 단일성이 보장됨
2) 통합 테스트 환경 기본 설정
•
환경설정 파일이 기본값이 아닌 경우, 추가 설정이 필요함. 관련 설정이 없으면 주요 의존성 주입이 실패에 따라 에러 발생
•
두 가지 방법으로 해결할 수 있음. 환경 지정 또는 환경설정 파일 지정
ex) 환경이 dev인 경우
@SpringBootTest
@WebAppConfiguration
@ActiveProfiles("dev")
class CartResourceIntegrationTest {
// 생략
}
Java
복사
ex) 환경설정 파일이 application-dev.yml 인 경우
@SpringBootTest
@WebAppConfiguration
@TestPropertySource(properties = { "spring.config.location=classpath:application-dev.yml" })
class UserResourceTest {
// 생략
}
Java
복사
•
작성한 테스트 코드를 IDE에서 찾을 수 없는 경우, 테스트 실행 환경을 변경해주는 것 권장
ex) ‘Run tests using’ 값을 Gradle → IntelliJ IDEA로 수정 후 정상
3) Spring Security 설정
Reference
•
•
이일민, 토비의 스프링 3, 에이콘
•
Spring IoC, Spring Bean, https://velog.io/@ljinsk3/Spring-IoC-컨테이너
•
Setting Multiple DataSources
◦
chainedTransactionManager 적용: https://stackoverflow.com/questions/30337582/spring-boot-configure-and-use-two-data-sources, https://www.javaskool.com/spring-boot-multiple-database-configuration-using-gradle/, https://sejoung.github.io/2021/04/2021-04-21-spring_boot_gradle_multi_modules/#The-dependencies-of-some-of-the-beans-in-the-application-context-form-a-cycle
◦
yaml 설정: https://oingdaddy.tistory.com/378
◦
테스트 설정: https://livenow14.tistory.com/79
•