스프링부트2

유요한·2023년 12월 1일
0

Spring Boot

목록 보기
6/25
post-thumbnail

스프링 부트 프로젝트 둘러보기

스프링 부트로 만든 애플리케이션을 실행하면 먼저 기본 배너가 출력되고 내장 톰캣이 구동된다.

이 배너를 변경할 수 있다. 또한 톰캣이 아닌 다른 서버를 사용하거나 서 포트 설정도 변경할 수 있다.

배너 변경하기

1) 배너 감추기

일단 배너를 보기 싫으면 배너 기능을 끄는 방법이 있다.

2) 배너 변경하기
자신이 원하는 스타일로 바꾸는 것이다.

톰캣 서버 포트 변경하기

스프링 부트가 제공하는 내장 톰캣 서버의 포트를 변경하고 싶으면 application.properties 파일에 서버 포트 관련 프로퍼티를 추가하면 된다.

이때 중요한 것은 톰캣 서버를 구동하여 웹 애플리케이션으로 실행하려면 application.properties 파일에서 web-application-type 설정을 servlet으로 다시 변경해야 한다는 것이다. 만약에 포트번호를 0으로 설정하면 현재 사용되지 않는 포트 번호가 랜덤으로 할당된다.


웹 애플리케이션 작성하기

컨트롤 빈 등록

우리가 스프링을 이용하여 개발하는 대부분의 시스템은 웹 애플리케이션이다. 따라서 스프링 부트도 웹 애플리케이션 관련된 다양한 기능을 제공한다. 실습을 통해 스프링 부트가 제공하는 웹 관련 기능들을 확인해보자

package com.example.book.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BoardController {
    public BoardController() {
        System.out.println("====> BoardController 생성");
    }
    
    @GetMapping("/hello")
    public String hello(String name) {
        return "hello : " + name;
    }
}

BoardController 위에는 @Controller가 아니라 스프링 4부터 지원하는 @RestController를 사용했다. @RestController를 사용하면 REST 방식의 응답을 처리하는 컨트롤러를 구현할 수 있다. 만약 @Controller를 사용했다면 hello() 메소드의 리턴 타입으로 문자열을 사용했을 때 문자열에 해당하는 view를 만들어야 한다. 하지만 컨트롤러를 Rest 컨트롤러로 등록하면 리턴되는 문자열이 브라우저에 그대로 출력되기 대문에 별도로 view 화면을 만들 필요가 없다.

@RestController

반환결과가 JSON 형태인 controller
@Controller + @ResponseBody

자동 컴포넌트 스캔

스프링 문법에 비추어보면 클래스 위에 @RestController를 설정했다 하더라도 XML 설정 파일에 <context:compnent-scan>을 설정하지 않으면 컨테이너가 컨트롤러를 빈으로 등록하지 않는다. 하지만 스프링 부트에서는 이 컴포넌트 스캔이 자동으로 처리되고 있다.

이 이유는 메인 클래스인 xxxApplication 클래스 위에 선언된 @SpringBootApplication에 있다. 여기에 들어가보면 많은 어노테이션들이 있다. 이중에서 @ComponentScan 어노테이션이 있다. 이 어노테이션이 기본적으로 main() 메소드가 포함된 xxxApplication 클래스가 속해 있는 패키지를 베이스 패키지로 하여 빈 등록을 처리한 것이다.

결국 이렇게 사용자가 정의한 클래스들이 자동으로 빈으로 등록되기 때문에 스프링 부트로 애플리케이션을 개발할 때는 패키지 이름을 주의해서 작성해야 한다. 즉, 루트 패키지가 아닌 다른 패키지에 클래스를 작성하면 스프링 컨테이너는 해당 클래스를 빈으로 등록하지 않는다.


Spring Boot Configuration Processor란?

@ConfigurationProperties를 사용하기 위한 의존성이다.
클래스에 @ConfigurationProperties를 지정하게 되면 application.yml 파일의 값을 읽어와서 멤버변수에 자동으로 할당한다. 이 때 application.yml의 key와 일치하는 멤버변수가 연결되는데 application.yml 파일의 key값이 user-id 과 같이 중앙 하이픈(-)이 포함된 경우 카멜표기법으로 변환된 key가 멤버변수와 연결된다.

다른 클래스에서 사용하기 위해서는 빈으로 등록되어야 하고 이에 @Component로 정의되어야 한다. @Setter가 있어야 application.yml 값이 자동주입된다. 주입된 데이터를 사용하려면 @Getter도 당연히 있어야 한다.

SpringBoot를 사용할 경우 @EnableConfigurationProperties를 정의할 필요가 없다. SpringBoot의 자동구성에 기본적으로 포함되어 있기 때문이다. SpringBoot 2.2 부터는 @ConfigurationProperties 클래스들을 모두 찾아서 등록해주므로 @Component 혹은 @Configuration과 같은 어노테이션이나 @EnableConfigurationProperties를 붙일 필요가 없다.


의존성 관리와 자동 설정

스프링 부트의 의존성 관리

스프링 부트를 구성하는 핵심 요소는 스타터(Starter), 자동설정(AutoConfiguration), 액추에이터(Actuator)다.

요소기능
스타터스프링이 제공하는 특정 모듈을 사용할 수 있도록 관련된 라이브러리 의존성을 해결한다.
자동설정스타터를 통해 추가한 모듈을 사용할 수 있도록 관련된 빈 설정을 자동으로 처리한다.
액추에이터스프링 부트로 개발된 시스템을 모니터링할 수 있는 다양한 기능을 제공한다.

스타터로 의존성 관리하기

스타터 이해하기

1) 프로젝트에 의존성 추가하기

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-entitymanager</artifactId>
			<version>5.4.2.Final</version>
		</dependency>

2) 스타터로 의존성 관리하기

이런식으로 메이븐 프로젝트에서는 pom.xml에서 프로젝트에서 필요한 의존성을 관리할 수 있다. 스프링과 JPA를 연동하기 위해서는 하이버네이트 말고도 spring-orm.jarspring-data-jpa.jar 같은 라이브러리들이 추가로 필요하다. 그런데 어떤 라이브러리가 필요하고 어떤 버전을 사용해야 하는지 미리 알기는 쉽지 않다. 이런 문제를 효율적으로 해결하기 위해서 제공하는 것이 바로 스프링 부트의 스타터다.

스타터는 필요한 라이브러리들을 관련된 것끼리 묶어서 마치 패키지 처럼 제공한다. 따라서 프로젝트에서 사용하고 싶은 모듈이 있으면, 그 모듈에 해당하는 스타터만 의존성으로 추가하면 된다. 그러면 관련된 라이브러리 의존성 문제가 자동으로 해결된다.

스프링 부트는 다양한 스타터들을 제공하며 spring-boot-start-모듈명 의 형태의 이름을 갖는 파일들이 바로 이런 스타터다. 이제 스프링 부트가 제공하는스타터를 이용하여 JPA 연동 설정을 처리해보자. pom.xml에 추가했던 hibernate-entitymanager에 대한 의존성은 삭제하고 다음과 같이 JPA 스타터로 변경한다.

	<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

스타터 사용하기

1) 기본 스타터

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.7</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>book</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>book</name>
	<description>book</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>
<!--		<dependency>-->
<!--			<groupId>org.springframework.boot</groupId>-->
<!--			<artifactId>spring-boot-starter-data-jpa</artifactId>-->
<!--		</dependency>-->

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<version>2.6.7</version>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

Devtools란 ?
devtools는 Spring boot에서 제공하는 개발 편의를 위한 모듈이다.
개발을 하다보면, 코드 수정시 브라우저에서 보여주는 내용도 수정하려면
어플리케이션을 재시작해야 하기 때문에 불편한 점이 많은데 devtools를 이용하면 이러한 불편한 점을 해결할 수 있다.

쉽게 말하면 브라우저로 전송되는 내용들에 대한 코드가 변경되면, 자동으로 어플리케이션을 재시작하여 브라우저에도 업데이트를 해주는 역할을 한다.

spring.devtools.livereload.enabled=true
spring.devtools.restart.enabled=true


먼저 Web은 스프링 MVC를 이용하여 웹 애플리케이션을 만들기 위해 추가한 스타터다. 스프링 MVC를 이용해서 아무리 간단한 웹 애플리케이션을 개발한다 하더라도 기본적으로 사용하는 라이브러리는 수십개가 넘는다. 하지만 이런 웹 애플리케이션 의존성을 일일이 설정하지 않고 간단하게 스타터로 처리한 것이다.

2) 스타터 설정 이해

스타터는 어떻게 최소한의 설정으로 수많은 라이브러리들을 자동으로 관리할 수 있을까? 비밀은 스타터에 있는 POM파일의 상속 구조에 있다.

여기에 들어가보면 수 많은 스프링 부트가 제공하는 수 많은 스타터들이 등록되어 있다. 그리고 스타터라는 이름으로 등록되지 않았지만 스프링 부트가 지원하는 의존성들도 엄청나게 많다.

이 수 많은 설정 중에 우리가 선택한 spring-boot-starter-web 스타터가 있으며, 이 설정에는 버전 정보까지 명시되 있어서 pom.xml에 버전 명시가 없어도 자동으로 이 버전 정보가 상속되었던 것이다.

POM 파일 상속 구조 이해하기

메이븐은 자바 클래스의 상속과 같이 상속을 통해 복잡한 설정을 재사용할 수 있는데, 이 때 사용하는 엘리먼트가 <parent>다. <parent> 엘리먼트는 이름 그대로 다른 POM 설정을 부모로 지정하여 부모로부터 모든 설정을 상속받을 때 사용한다.

의존성 재정의하기

스타터 재정의하기

부모로부터 상속된 의존성은 자식 프로젝트에서 재정의 할 수 있다. 현재 프로젝트에서 사용중인 pom.xml 파일에 등록된 스타터 설정들을 보면 스타터에 대한 <version> 정보가 없다. 이는 부모로부터 버전 관련 <properties> 설정이 상속되었기 때문이다. 스타터의 버전을 변경하면 스타터가 관리하는 수 많은 의존성 역시 호환 가능한 버전으로 자동으로 변경된다.

프로퍼티 재정의하기

스프링 부트의 스타터는 관련된 라이브러리들을 묶음으로 관리한다고 햇다. 따라서 스프링 MVC를 이용하여 웹 애플리케이션을 개발하는 경우에는 spring-boot-starter-web에 대한 의존성만 추가하면 된다. 그런데 만약 spring-boot-starter-web이 제공하는 라이브러리들 중에서 스프링 프레임워크 버전만 변경하고 싶으면 어떻게 해야 할까?

이런 경우에는 부모로부터 상속받은 프로퍼티를 재정의하면 된다. pom.xml 파일을 열어서 <properties>에 스프링 버전에 해당하는 프로퍼티를 추가한다.


스프링 부트의 자동설정

자동 설정 이해하기

자동설정이란?

스프링 부트로 만든 프로젝트에는 애플리케이션 실행을 위한 메인 클래스가 기본적으로 제공된다. 이 메인 클래스를 실행하면 내장 톰캣이 구동되고 스프링 기반의 웹 애플리케이션이 잘 동작하는 것을 확인할 수 있다. 하지만 스프링 MVC를 이용해서 웹 애플리케이션을 개발할 때는 여러가지 설정이 필요하다.

하지만 이런 복잡한 설정 없이도 웹 애플리케이션을 만들고 실행할 수 있었다. 이런 것이 가능한 이유는 스프링 부트가 제공하는 자동설정 기능이 동작하여 수 많은 빈들이 등록하고 동작했기 때문이다. 이 비밀은 메인 클래스 위에 선언된 @SpringBootApplication에 있다.

그러면 어떻게 스프링 부트는 메인 클래스의 @SpringBootApplication 하나만으로 복잡한 설정들을 대신할 수 있었을까? 사실은 @EnableAutoConfiguration 어노테이션 때문이다.

@SpringBootApplication에 들어가면 많은 어노테이션 중에서 중요한 것은 @SpringBootConfiguration, @ComponentScan, @EnableAutoConfiguration이다.

@SpringBootConfiguration은 @Configuration과 동일하다. 단지 이 클래스가 스프링 부트 환경설정 클래스임을 표현하기 위해 이름만 @SpringBootConfiguration로 표현한 것이다. 나머지 두 개는 초기화와 관련된 어노테이션인데, 먼저 @ComponentScan은 @Configuration, @Repository, @Service, @RestController, @Controller가 붙은 객체를 메모리에 올려주는 역할을 한다.

@EnableAutoConfiguration은 자동설정과 관련된 어노테이션이다. 스프링 부트는 스프링 컨테이너를 구동할 때 두 단계로 나누어 객체들을 초기화(생성)한다. 스프링 부트가 이렇게 두 단계로 나누어 빈들을 초기화하는 이유는 애플리케이션을 운영하기 위해서는 두 종류의 빈들이 필요하기 때문이다.

예를들어, 웹 애플리케이션에서 파일 업로드 기능을 추가한다고 가정하자면 파일 업로드를 추가하기 위해서는 먼저 BoardController 같은 컨트롤러를 MultipartFile 객체를 이용해서 업로드 가능한 컨트롤러로 구현해야 합니다. 그런데 실제로 파일 업로드 기능이 동작하기 위해서는 반드시 사용자가 업로드한 파일 정보가 MultipartFile 객체에 설정되어 있어야 하며, 이를 위해서 멀티파트 리졸버 객체가 반드시 필요합니다. 즉, 파일 업로드가 정상적으로 동작하기 위해서는 내가 만든 컨트롤러뿐만 아니라 이를 위해 멀티파트 리졸버 객체를 메모리에 올리는 두 개의 객체 생성 과정이 필요한 것이다. 결국 @ComponentScan은 내가 만든 컨트롤러 객체를 메모리에 올리는 작업을 처리하고 @EnableAutoConfiguration은 CommonsMultipartResolver 같은 객체들을 메모리에 올리는 작업을 처리한다.

@Configuration은 이클래스가 스프링 빈 설정 클래스임을 의미한다. 따라서 @ComponentScan이 처리될 때 자신뿐만 아니라 이 클래스에 @Bean으로 설정된 모든 빈들도 초기화된다.

@ConditionalOnWebApplication은 웹 애플리케이션 타입이 어떻게 설정되어 있느냐를 확인하는 어노테이션이다.

@ConditionalOnClass는 특정 클래스가 클래스 패스에 존재할 때, 현재 설정 클래스를 적용하라는 의미다.


어노테이션

  • 자바 어노테이션은 자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종이다. 보통 @를 앞에 붙여서 사용한다. 자바 어노테이션은 클래스 파일에 임베디드되어 컴파일러에 의해 생성된 후 자바 가상머신에 포함되어 작동한다.

→ 예전에는 자바 코드와 설정 파일을 따로 두고 저장하고 관리했지만 이와 같은 경우는 두 가지의 어려움이 있다. 첫째는 자바 코드는 변경하는데 설정파일을 업데이트 하지 않는 어려움과 두 번째는 설정과 코드가 나뉘어 있어서 개발이 어렵다. 그래서 다음과 같은 관리방법을 채택했다.

이렇게 하면 하나의 파일에서 코드와 설정을 관리할 수 있게 된다.

어노테이션의 종류

  1. @Override
    오버라이딩을 올바르게 했는지 컴파일러가 체크한다.
    Override는 오버라이딩할 때, 메서드의 이름을 잘못적는 실수를 방지
  1. @Deprecated
    앞으로 사용하지 않을 것을 권장하는 필드나 메소드에 붙인다.
  1. @SuppressWarnings
    컴파일러의 경고메세지가 나타나지 않게 한다. 보통 경고가 많을 때, 확인된 경고는 해당 어노테이션을 붙여서 새로운 경고를 알아보지 못하는 것을 방지하기 위해 사용한다.

  2. @Component
    @Component 어노테이션은 클래스에 선언하는 어노테이션이다. 이 어노테이션을 선언해주는 것만으로도 해당 클래스를 스프링 빈(bean) 객체로 사용할 수 있다. Spring MVC역시 Spring context에서 동작하기 때문에 사용할 모든 클래스를 빈으로 등록해야 할 필요가 있다. Controller 역할을 하는 클래스 역시 마찬가지다.

  3. @Controller
    @Controller는 Controller 클래스에 붙이는 것이다. Controller 클래스는 클라이언트와의 요청, 응답을 주고 받는 역활을 한다. 전통적인 Spring MVC의 컨트롤러인 @Controller는 주로 View를 반환하기 위해 사용합니다.

이 역활을 수행하기 위해서 Controller 클래스는 최소 두 가지의 조건을 만족시켜야 한다.

  1. HttpServlet의 기능을 수행해야 할 것.
  2. url(호출)-클래스(서버)-화면(응답)이 서로 연결되어야 할 것.

이 두가지 기능과 함께 @Controller@Component@RequestMapping의 역할을 모두 할 수 있다.

정확하게 말하자면 이 두 가지 기능을 수행하는 것은 @Controller만 할 수 있는 것은 아니다. @RequestMapping가 이 두 가지 기능을 할 수 있고 @Component가 해당 클래스를 스프링 빈으로 생성할 수 있게 해주는 것이다.

즉, @Component + @RequestMapping = @Controller다.

  1. @Service

비즈니스 로직을 담당하는 서비스 클래스임을 의미하게 해준다.

  1. @RestController

RestController는 Controller에서 @ResponseBody 어노테이션이 붙은 효과를 지니게 됩니다. 즉, 주용도는 JSON/XML형태로 객체 데이터 반환을 목적으로 합니다.

  1. @InitBinder

파라미터의 수집을 다른 용어로는 binding(바인딩)이라고 합니다. 변환이 가능한 데이터는 자동으로 변환되지만 경우에 따라서는 파라미터를 변환해서 처리해야 하는 경우도 존재합니다. 예를 들어, 화면에서 '2018-01-01'과 같이 문자열로 전달된 데이터를 java.util.Data 타입으로 변환하는 작업이 그러합니다. 스프링 Controller에서는 파라미터를 바인딩할 때 자동으로 호출되는 @InitBinder를 이용해서 이러한 변환을 처리할 수 있습니다.

TodoDTO.java

package com.example.domain;

import lombok.Data;

import java.util.Date;

@Data
public class TodoDTO {
    private String title;
    private Date dueDate;
}

TodoDTO에는 특별하게 dueDate 변수의 타입이 java.util.Date 타입입니다. 만일 사용자가 2018-01-01과 같이 들어오는 데이터를 변환하고자 할 때 문제가 발생하게 됩니다. 이러한 문제를 해결하는 방법이 @InitBinder를 이용하는 것입니다.

SampleController.java

    @InitBinder
    public void initBinder(WebDataBinder binder) {
       SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
      binder.registerCustomEditor(java.util.Date.class, new CustomBooleanEditor(dateFormat, false));
  }
  1. @DateTimeFormat

파라미터로 사용되는 인스턴스 변수에 @DateTimeFormat을 적용해도 변환이 가능합니다. 이 어노테이션을 사용하는 경우 @InitBinder는 필요하지 않습니다.

TodoDTO

package com.example.domain;

import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;

@Data
public class TodoDTO {

    private String title;

    // 이 어노테이션을 사용하면 @InitBinder을 안써도 된다.
    @DateTimeFormat(pattern = "yyyy/MM/dd")
    private Date dueDate;
}

SampleController

   @GetMapping("/ex06")
    public String ex06(TodoDTO todo) {
        log.info("todo : " + todo);
        return "ex06";
    }
  1. @Autowired

Lombok 어노테이션

어노테이션설명
@Getter/Setter코드를 컴파일할 때 속성들에 대한 Getter/Setter 메소드 생성
@ToStringtoString() 메소드 생성
@ToString(exclude={"변수명"})원하는 않는 속성을 제외한 toString() 메소드 생성
@NonNull해당 변수가 null 체크, NullPointerException 예외 발생
@EqualsAndHashCodeequals()와 hashCode() 메소드 생성
@Builder빌더 패턴을 이용한 객체 생성
@NoArgsConstructor파라미터가 없는 기본 생성자
@AllArgsConstructor모든 속성에 대한 생성자 생성
@RequiredArgsConstructor초기화되지 않은 Final, @NonNull 어노테이션이 붙은 필드에 대한 생성자 생성
@Loglog 변수 자동 생성
@Value불변 클래스 생성
@Data@ToString, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgConstructor를 합친 어노테이션

환경설정

Spring Initializr에서 설정

  1. group
    기업 도메인 명

  2. Artifact
    빌드된 결과물(프로젝트 이름)

  3. 자바 버전 : 11

  4. jar

  5. 추가해줘야 할것

  • lombok
  • spring web
  • tymeleaf
  1. spring boot 버전 : 2.7.7
    3.1로 했을 경우 에러가 발생

  2. gradle은 gradle-groovy로 만들면 된다.


파일 구조


1) templates : 스프링이 버전이 올라가면서 tyhmeleaf를 사용하는데 thymeleaf파일을 두는 곳이다.

2) static : content들을 두는 곳이다. css나 js를 둔다.

다음을 사용하여 빌드 및 실행과 테스트 실행 모두 인텔리제이로 바꿔줘야 한다.

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.7'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
// 자바 11버전
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation ('org.springframework.boot:spring-boot-starter-test') {
        exclude group : 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

tasks.named('test') {
    useJUnitPlatform()
}

dependencies에 쓰면 알아서 외부 라이브러리들을 받아준다. 연관된 라이브러리들도 받아줘서 편하다.


스프링 부트가 제공하는 기능

Welcome Page 기능

static/index.html 을 올려두면 Welcome page 기능을 제공한다

<!DOCTYPE HTML>
<html>
<head>
   <title>Hello</title>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
Hello
<a href="/hello">hello</a>
</body>
</html>

thymeleaf 탬플릿

package com.example.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HelloController {
    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("data", "hello!");
        return "hello";
    }
}

hello.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <title>Hello</title>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
</body>
</html>

동작 환경

컨트롤러에서 리턴 값으로 문자를 반환하면 viewResolver가 화면을 찾아서 처리한다.

  • 스프링 부트 템플릿엔진 기본 viewName 매핑
  • resources:templates/ +{ViewName}+ .html

참고: spring-boot-devtools 라이브러리를 추가하면, html 파일을 컴파일만 해주면 서버 재시작 없이 View 파일 변경이 가능하다.
인텔리J 컴파일 방법: 메뉴 build Recompile


빌드하고 실행하기

gradlew(gradle wrapper)란?

  • gradle wrapper 줄여서 gradlew는 새로운 환경에서 프로젝트를 설정할 때 java나 gradle을 설치하지 않고 바로 빌드할 수 있게 해주는 역할을 한다.

  • gradlew 는 shell script 이며 gradlew.bat는 Window batch script 이다.

  • gradlew 를 사용하는 가장 큰 이유는 아래와 같이 로컬환경에서 빌드할 경우 로컬 환경에 설치된 java와 gradle 버전에 영향을 받게 된다.

  • gradlew 를 이용하여 빌드하면 로컬 환경 java와 gradle 버전과 상관없이 새로운 프로젝트를 빌드할 수 있다.

서버를 배포할 때 $ java -jar hello-spring-0.0.1-SNAPSHOT.jar을 넣으면 된다.


정적 컨텐츠

스프링 부트는 정적 컨텐츠를 자동으로 제공을 한다.


MVC와 템플릿 엔진

MVC
Model, View, Controller

  • 컨트롤러: 웹 MVC의 컨트롤러 역할
  • 서비스: 핵심 비즈니스 로직 구현
  • 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
  • 도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨

  • 아직 데이터 저장소가 선정되지 않아서 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
  • 데이터 저장소는 RDB, NoSQL 등 다양한 저장소를 고민중인 상황으로 가정
  • 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용

package com.example.hellospring.domain;

public class Member {
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;

import java.util.List;
import java.util.Optional;

public interface MemberRepositrory {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}
package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.apache.catalina.Store;

import java.util.*;

public class MemoryMemberRepository implements MemberRepositrory {
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        //
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        // 람다
        return store.values().stream()
                // member의 name이 파라미터로 넘어온 name이랑 같은 경우에만 필터링된다.
                .filter(member -> member.getName().equals(name))
                // 루프를 다 돌면서 하나라도 찾으면 반환을 한다.
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

package com.example.hellospring.service;

import com.example.hellospring.domain.Member;
import com.example.hellospring.repository.MemberRepositrory;
import com.example.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {

   private final MemberRepositrory mr = new MemoryMemberRepository();

   // 회원가입
   public long join(Member member) {
       // 같은 이름이 있는 중복 회원 x
       validateDuplicateMember(member); // 중복 회원 검증
       mr.save(member);
       return member.getId();
   }

   private void validateDuplicateMember(Member member) {
       mr.findByName(member.getName())
           .ifPresent(m ->  {
               throw new IllegalStateException("이미 존재하는 회원입니다.");
           });
   }

   // 전체 회원 조회
   public List<Member> findMembers() {
        return mr.findAll();
   }

   public Optional<Member> findOne(long memberId) {
       return mr.findById(memberId);
   }


}
  package com.example.hellospring.service;

import com.example.hellospring.domain.Member;
import com.example.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService = new MemberService();
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void join() {
        // given
        Member member = new Member();
        member.setName("hello");

        // when
        long saveId = memberService.join(member);

        // then
        Member fineMember = memberService.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(fineMember.getName());
    }

    @Test
    public void 중복_회원_예외() {
        // given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        // when
        memberService.join(member1);

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        try {
//            memberService.join(member2);
//            fail();
//        } catch (IllegalStateException e) {
//            Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }
        // then
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

  package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;

public class JdbcTemplateMemberRepository implements MemberRepository {

    private  final JdbcTemplate jdbcTemplate;

    public JdbcTemplateMemberRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        return null;
    }

    @Override
    public Optional<Member> findById(Long id) {
        List<Member> result  = jdbcTemplate.query("select * from test_member where id = ?", memberRowMapper());
        return result.stream().findAny();
    }

    @Override
    public Optional<Member> findByName(String name) {
        return Optional.empty();
    }

    @Override
    public List<Member> findAll() {
        return null;
    }

    private RowMapper<Member> memberRowMapper() {
        return (RowMapper<Member>) (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}
profile
발전하기 위한 공부

0개의 댓글