Open skarltjr opened 3 years ago
-메이븐 의존성추가는 https://mvnrepository.com/에서 확인할 수 있다.
-스프링부트가 관리해주는 의존성은 버전을 명시하지않아도 되지만 관리하지 않는것은 반드시 버전까지 명시할것 ex) modelMapper -> 스터디 웹 서비스에서도 pom.xml에 dependency를 추가해준 후 매번 만들어서 사용하기보단 AppConfig에 빈으로 등록해서 편하게 사용했다.
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.1.0</version>
</dependency>
기존 의존성을 변경할 땐 ex)스프링의 버전을 바꾸고 싶다.
<properties>
<spring.version>5.0.6.RELEASE</spring.version>
</properties>
처럼 properties로 설정해준다
-@SpringBootApplication은 두 단계를 통해 빈을 읽는다 ○ 1단계: @ComponentScan ○ 2단계: @EnableAutoConfiguration //dependecy로 의존성 추가해준것들 등을 자동으로
-@ComponentScan : basePackages 프로퍼티 값에 별도의 경로를 설정하지 않으면 해당 어노테이션이 위치한 패키지가 루트 경로가 된다. 하위 루트까지 쭉 가면서 Bean을 등록한다.
-@Component 가 붙어있는 클래스를 찾아가서 모든 인스턴스를 생성해 빈으로 등록한다. @Configuration @Repository @Service @Controller @RestController@Component 전부
-@EnableAutoConfiguration : 미리 정의된 Bean을 가져와서 등록해준다. 미리 정의된 Bean은 외부 라이브러리 중 sring-boot-autoconfigure에 META-INF 디렉토리 하위의 spring.factories에 자동으로 가져올 Bean들이 등록되어있다.
★ 주의해야할 점은 1단계: @ComponentScan -> 2단계: @EnableAutoConfiguration 즉 만약 의존성추가로 생성한(autoconfig) BeanA가 내가 직접등록한 빈끌어온것(componentscan)이랑 중복,겹친다면 직접등록한 빈이 항상 우선시되어야한다. 그래서 만약 직접등록한 빈이 있으면 겹치는경우 자동설정으로 덮어씌우지 못하도록 @ConditionalOnMissingMean추가(autoconfiguration대상에) (내가 등록한 빈이 항상 우선시되어야한다 componentscan > autoconfig자동설정)
@Data
@Component
@ConfigurationProperties("app")` //"app"으로 이름을 지정해주고
public class AppProperties {
` private String host;`
}
//# 웹 서버 호스트 가져온다
//app.host=http://localhost:8080
-> 등록된 빈의 부분부분을 재정의 하고싶을 때 @ConfigurationProperties + properties에서 app.host=http://localhost:8080 -> 위에서는 component로 빈으로 등록했고 예를들어 8080포트말고 다른포트를 쓸 때 이 빈을 재정의할 필요없이 application.properties에서 app.name에 대해서 값만 변경해주면 완료
이런방법을 사용할 땐
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
추가해줘야한다
○ 톰캣 객체 생성 ○ 포트 설정 ○ 톰캣에 컨텍스트 추가 ○ 서블릿 만들기 ○ 톰캣에 서블릿 추가 ○ 컨텍스트에 서블릿 맵핑 ○ 톰캣 실행 및 대기 이 순서로 서버를 띄우는데 스프링부트가 이 모든 과정을 유연하게 설정해는것이 스프링부트의 자동설정
스프링부트의 자동설정파일들은 External libraries -> autoconfigure-> META INF -> spring factories에 있다 다만 서버를 만들 때 당연히 서블릿컨테이너는 여러 종류가있다 가령 톰캣, 제티, 언더토 등등 그러나 디스패처서블릿은 단 한종류. 그래서 내부에서 서블릿컨테이너 만드는코드와 디스패처서블릿을 만드는 코드는 분리되어있다.
@ConfigurationProperties
사용에 대한 간단한 예시를 만들어보면
@Component
@ConfigurationProperties("test")
public class TestClass {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@RestController public class FirstController { private TestClass testClass;
@Autowired
public FirstController(TestClass testClass) {
this.testClass = testClass;
}
@GetMapping("/hello")
public String hello() {
return testClass.getName();
}
}
test.name="kiseok"
-스프링부트는 자동설정으로 서블릿컨테이너(서버)를 톰캣을 사용한다. 만약 제티,언더토 등으로 바꾸고 싶다면
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
server.port=7070
-랜덤포트번호를 사용하고 싶을 떈
server.port=0
HTTPS설정하기
터미널에서 아래와 같이 실행 // 아래 spring은 그냥 별칭 아무거나 keytool -genkey -alias spring -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 4000 Enter keystore password: Re-enter new password: What is your first and last name? Unknown: KISEOK NAM What is the name of your organizational unit? What is the name of your organization? What is the name of your City or Locality? What is the name of your State or Province? What is the two-letter country code for this unit? Is CN=KISEOK NAM, OU=soongsil, O=stu, L=seoul, ST=seoul, C=KR correct?
즉 로컬에서 인증서를 만드는 것 (keystore)
properties에 설정
test.name="kiseok"
server.ssl.key-store=keystore.p12
server.ssl.key-store-password=123456
server.ssl.keyStoreType=PKCS12
server.ssl.keyAlias=spring
https로 설정하면 http론 당연히 못들어간다 / 스프링부트는 톰캣이사용하는 컨넥터를 하나만 등록해주고 위처럼 적용하면 커넥터에 ssl을 적용해준다. 따라서 모든요청을 https를 붙여서 https://localhost:8080 가보면 메세지가 뜰텐데 사실 당연히 이건 로컬에서 멋대로 설정한 인증서같은거라 공인인증서가 아니다,.
server.http2.enabled=true
해주면 http2가 설정이 된다.
확인을 해보면 개발자도구 -> 네트워크 -> 프로토콜을 보면 h2인것을 확인할 수 있었다.스프링부트의 핵심 , goal
-독립적으로 실행가능한 jar
-mvn package 혹은 인텔리제이 오른쪽 maven에서 lifecycle->package를 수행하면 packaging
-> C:\Users\남기석\Desktop\pro\Spring_boot\demo\target에 jar가 생성 -> 새로운 폴더 ex)app을 만들어주고 거기에 이
jar파일 압축을 풀어주고 인텔리제이에서 확인해보면 target 아래 app에서 MANIFEST.MF가 있는 걸 확인.
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: spring_boot.demo.DemoApplication
Spring-Boot-Version: 2.2.10.RELEASE
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
동작하는지 확인을 해보면 터미널에서 자르파일을 실행해보면 잘 동작하는 것을 확인할 수 있었다
java -jar demo-0.0.1-SNAPSHOT.jar
stop할 땐 foreground로 실행시켰기 때문에 컨트롤c or kill -9 cat boot.pid
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//SpringApplication.run(DemoApplication.class, args);
/** 위와 동일하지만 배너 등 다양한 설정 변경가능*/
SpringApplication springApplication = new SpringApplication(DemoApplication.class);
springApplication.run(args);
}
}
-애플리케이션 리스너
//@Component
public class SampleListener implements ApplicationListener<ApplicationStartingEvent> {
@Override
public void onApplicationEvent(ApplicationStartingEvent applicationStartingEvent) {
System.out.println("=======================");
System.out.println("Kiseok's Application starts");
System.out.println("=======================");
}
/** 주의
* 애플리케이션 컨텍스트가 올라간 후에는 빈으로 등록한 것들이 있으면 끌어와서 설정
* 그런데 지금 이 경우는(ApplicationStartingEvent) 애플리케이션 컨텍스트(스프링컨테이너)가 올라가기전
* 즉 맨처음에 실행. 그래서 아무리 빈으로 등록해봤자 적용이 안된다. 그래서
*
* SpringApplication springApplication = new SpringApplication(DemoApplication.class);
* springApplication.addListeners(new SampleListener()); 리스너를 추가해준다
* springApplication.run(args);
*
* 사실상 빈으로 등록하는 의미자체도 없다 지운다
* =======================
* Kiseok's Application starts
* =======================
*
* 그러나 만약 ApplicationStartingEvent -> ApplicationStartedEvent로 바꿔보면
* ApplicationStartedEvent는 애플리케이션 컨텍스트가 올라간 후 실행되는 이벤트이기 때문에
* 그냥 리스너를 Component로 빈으로 올려주기만하면 된다. addListener할 필요없다
* */
}
프로퍼티 우선 순위
유저 홈 디렉토리에 있는 spring-boot-dev-tools.properties
테스트에 있는 @TestPropertySource
@SpringBootTest 애노테이션의 properties 애트리뷰트
커맨드 라인 아규먼트
SPRING_APPLICATION_JSON (환경 변수 또는 시스템 프로티) 에 들어있는 프로퍼티
ServletConfig 파라미터
ServletContext 파라미터
java:comp/env JNDI 애트리뷰트
System.getProperties() 자바 시스템 프로퍼티
OS 환경 변수
RandomValuePropertySource
JAR 밖에 있는 특정 프로파일용 application properties
JAR 안에 있는 특정 프로파일용 application properties
JAR 밖에 있는 application properties
JAR 안에 있는 application properties
@PropertySource
기본 프로퍼티 (SpringApplication.setDefaultProperties)
샘플러너를 만들고 @Value를 통해 properties에서 값을 지정해줬을 때
@Component
public class SampleRunner implements ApplicationRunner {
@Value("${kiseok.name}") // -> properties에서 설정
private String name;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("================");
System.out.println(name);
System.out.println("================");
}
}
->properties에서 값 지정
kiseok.name = kiseok2
-> 테스트에서 확인
@SpringBootTest
class DemoApplicationTests {
@Autowired
Environment environment; // properties에 있는 정보 가져올 수 있다
@Test
void contextLoads() {
assertThat(environment.getProperty("kiseok.name"))
.isEqualTo("kiseok2");
}
}
kiseok2가 나오는 것을 확인할 수 있다
★★ 만약에 테스트에서 resources아래 테스트용 properties를 만든다면 주의해야할 것 테스트에서는 테스트의 properties가 기존 properties를 덮어씌운다 . / 이를 방지하기위해선 그냥 테스트 properties를 지우면 자동으로 기존 properties를 따른다
우선순위에 따라 비교해보기 위해 테스트의 @SpringBootTest 애노테이션의 properties 애트리뷰트
-> properties에는 여전히kiseok.name = kiseok2
이지만 아래 테스트가 통과하는걸 확인할 수 있다.
@SpringBootTest(properties = "kiseok.name = kiseok3")
class DemoApplicationTests {
@Autowired
Environment environment; // properties에 있는 정보 가져올 수 있다
@Test
void contextLoads() {
assertThat(environment.getProperty("kiseok.name"))
.isEqualTo("kiseok3");
}
}
왜냐 ? -> @SpringBootTest(properties = "kiseok.name = kiseok3")가 프로퍼티설정보다 높은 우선순위를 갖기 때문 애초에 properties에서 설정하는 것들은 우선순위가 매우 낮다.
++ application.properties 자체의 우선 순위 (높은게 낮은걸 덮어 쓴다.)
->->
@Component
@ConfigurationProperties("app")
public class SampleRunner implements ApplicationRunner {
//@Value("${kiseok.name}") // -> properties에서 설정
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("================");
System.out.println(name);
System.out.println("================");
}
}
app.name=kiseok4
================ kiseok4 ================ 나오는 걸 확인할 수 있다.
-@Profile
@Configuration
@Profile("prod")
public class BaseConfiguration {
@Bean
public String hello() {
return "hello";
}
}
@Configuration
@Profile("test")
public class TestConfiguration {
@Bean
public String hello() {
return "hello test";
}
}
SampleRunner 에서 hello를 주입받아 사용하면 hello가 출력될까?
@Component
@ConfigurationProperties("app")
public class SampleRunner implements ApplicationRunner {
//@Value("${kiseok.name}") // -> properties에서 설정
private String name;
@Autowired
private String hello;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("================");
System.out.println(hello);
System.out.println("================");
}
}
spring.profiles.active=prod
그래서 이 Profile을 어떻게 활용하냐?
@Profile 애노테이션은 어디에?
● @Configuration
● @Component
그렇다면
@Configuration
@Profile("prod")
public class BaseConfiguration {
@Bean
public String hello() {
return "hello";
}
}
@Configuration
@Profile("test")
public class TestConfiguration {
@Bean
public String hello() {
return "hello test";
}
}
spring.profiles.active=prod
---> application-prod.properties
spring.profiles.active=test
----> application-test.properties
일 때
@Component @ConfigurationProperties("app") public class SampleRunner implements ApplicationRunner { //@Value("${kiseok.name}") // -> properties에서 설정 private String name; @Autowired private String hello;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("================");
System.out.println(name);
System.out.println(hello);
System.out.println("================");
}
}
================
kiseok4
hello test
================
-테스트
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)//<-기본값 ->굳이 서블릿컨테이너를 띄우지않고도 대신 Mockmvc가 필요
@AutoConfigureMockMvc
class SampleControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void hello() throws Exception {
mockMvc.perform(get("/hellohello"))
.andExpect(status().isOk())
.andExpect(content().string("hello kiseok"));
}
}
● webEnvironment ○ MOCK: mock servlet environment. 내장 톰캣 구동 안 함. ○ RANDON_PORT, DEFINED_PORT: 내장 톰캣 사용 함. ○ NONE: 서블릿 환경 제공 안 함.
-테스트를 최적화, 가볍게하기위해 = 슬라이스 테스트
예를들어 컨트롤러에 대해서만 테스트를 하기위해 쪼갠다 -> @WebMvcTest(~Controller.class) -> 그러나 슬라이스 테스트시 컨트롤러에 대해서만 하기위해 서비스,레퍼지토리등은 빈으로 올려놓지 않는다 -> 그래서 MockBean로 추가해줘야한다.
//@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)//<-기본값 ->굳이 서블릿컨테이너를 띄우지않고도 대신 Mockmvc가 필요
//@AutoConfigureMockMvc
@WebMvcTest(SampleController.class)
class SampleControllerTest {
@MockBean SampleService sampleService;
@Autowired
private MockMvc mockMvc;
@Test
void hello() throws Exception {
when(sampleService.getName()).thenReturn("kiseok");
mockMvc.perform(get("/hellohello"))
.andExpect(content().string("hello kiseok"));
}
}
슬라이스 테스트 ● 레이어 별로 잘라서 테스트하고 싶을 때 ● @JsonTest ● @WebMvcTest ● @WebFluxTest ● @DataJpaTest
-스프링 부트와 웹mvc // + mvc 복습
-spring factories -> WebMvcAutoConfiguration.class -> WebMvcProperties.class에 가보면
@ConfigurationProperties(
prefix = "spring.mvc"
)
@ConfigurationProperties로 웹mvc에 대한 다양한 설정정보를 properties에서 설정할 수 있도록 해준다.
Httpmessageconverter 유저 클래스
public class User {
private Long id;
private String username;
private String password;
getter setter }
초간단 컨트롤러
@PostMapping("/users/create")
public User create(@RequestBody User user) {
return user;
}
본문내용을 User로 받아오고 리턴도 본문에 user로
-테스트
@Test
void createUser_JSON() throws Exception {
String userJson="{\"username\" : \"kiseok\",\"password\":\"123\"}"; //json본문 내용
mockMvc.perform(post("/users/create")
.contentType(MediaType.APPLICATION_JSON) //응답할 때
.accept(MediaType.APPLICATION_JSON) //받을 때
.content(userJson))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username", is(equalTo("kiseok"))))
.andExpect(jsonPath("$.password", is(equalTo("123"))));
}
-리소스핸들러 추가하기
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/m/**") // m디렉토리 하위에 대해 처리하도록 리소스핸들러추가
.addResourceLocations("/m/") // /m 디렉토리 하위 파일에 대해서
.setCachePeriod(20);
//그럼 resources아래 static디렉토리말고 m디렉토리 추가해줘서 그 하위 파일에 대해 /m/~ 요청으로
//들어오는 애들은 /m디렉토리에서 찾아서 해결
}
}
mockMvc와 서블릿컨테이너
@Test
public void viewTest() throws Exception {
mockMvc.perform(get("/view"))
.andExpect(status().isOk())
.andExpect(view().name("view"))
.andExpect(model().attributeExists("name"))
//여기까지는 뷰 내부를 작성하지않고 뷰만 만들어놔도 동작 -> mockmvc는 실제 서블릿컨테이너를 띄우는게아니라
// 서블릿컨테이너가 하는역할을 다 수행 x -> 뷰에 어떻게 렌더링하는지까지는 테스트 불가 그래서
.andExpect(content().string(containsString("kiseok")));
}
html을 테스트하기위한 HtmlUnit도 있다
-ExceptionHandler
@Slf4j
@ControllerAdvice // ControllerAdvice를 통해 (전역 컨트롤러)즉 모든 컨트롤러에서 전반적으로 로그를 남기기 위해
public class ExceptionAdvice {
/** 런타임동안 누가 어떤 요청으로 에러를 발생시킨건지 로그 남기기*/
@ExceptionHandler //전역컨트롤러를 통해 모든 에러에 대해서
public String handleRuntimeException(@CurrentUser Account account, HttpServletRequest req, RuntimeException e) {
if (account != null) {
log.info("'{}' requested '{}'", account.getNickname(), req.getRequestURI());
//누가 어떤 요청을 보냈는지 로그남기기
} else {
log.info("requested '{}'", req.getRequestURI());
}
log.error("bad request", e);
return "error";
}
}
스프링 부트가 제공하는 기본 예외 처리기 ● BasicErrorController ○ HTML과 JSON 응답 지원 ● 커스터마이징 방법 ○ ErrorController 구현
혹은 커스텀 에러 페이지 ● 상태 코드 값에 따라 에러 페이지 보여주기 ● src/main/resources/static|template/error/ //아래에서 ● 404.html // 특정에러 상태값에 대한 html 페이지를 만들어서 ● 5xx.html ● ErrorViewResolver 구현
여기에 걸리지 않는 최종적인 에러는 결국 BasicErrorController로 처리
Springboot hateoas
{
"content":"Hello, World!",
"_links":{
"self":{
"href":"http://localhost:8080/greeting?name=World"
}
}
}
처럼 RESP API를 위해 본문에 컨텐트뿐만 아니라 link정보를 추가할 수 있도록 도와준다
그래서 resource를 만들고
public class HateoasSample {
private String prefix;
private String name;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return prefix + " " + name;
}
}
컨트롤러에서 사용
@RestController
public class HateoasController {
@GetMapping("/hateoas")
public EntityModel<HateoasSample> hateoas() {
HateoasSample hateoasSample = new HateoasSample();
hateoasSample.setName("kiseok");
hateoasSample.setPrefix("hey");
//이대로면 링크정보가 없으니 테스트 실패할테니 링크정보를 추가해줘야하는데
EntityModel<HateoasSample> hateoasModel = new EntityModel<>(hateoasSample); //resource를 만들고
hateoasModel.add(linkTo(methodOn(HateoasController.class).hateoas()).withSelfRel());
//linkTo로 만든 링크정보추가
return hateoasModel; // 그럼 리턴도 리턴타입도 변경해줘서 리턴
}
}
이때 import는
import org.springframework.hateoas.EntityModel;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
SOP과 CORS ● Single-Origin Policy ● Cross-Origin Resource Sharing ● Origin? ○ URI 스키마 (http, https) ○ hostname (whiteship.me, localhost) ○ 포트 (8080, 18080) //ex) 8080포트를 사용하는 어플리케이션에서 restcontoller로 18080포트를 사용하는 다른 애플리케이션의 리소스를 요청하면 //안된다. 그래서 그걸 가능하도록 우회하는방법
● @Controller나 @RequestMapping에 추가하거나 ex)
@Getmapping("/hello")
@CrossOrigin(origins = "http://localhost:18080")
근데 여러곳에서 필요하다면 매 번 컨트롤러에서 어노테이션붙여서 사용하기보단 ● WebMvcConfigurer 사용해서 글로벌 설정 addCorsMapping 구현 -> registry에 addMapping + allowedOrigins으로
기본적으로 시큐리티 적용하려면 Configuration + WebSecurityConfigurerAdapter 예를들어
@Configuration
//@EnableWebSecurity을 사용하면 기존 시큐리티 기본설정은 사용하지않는다 . 원래 기본설정을 잘 안쓴다
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/hello").permitAll() //루트와 hello는 로그인필요없이
.anyRequest().authenticated() //그외 나머지는 모두 인증절차필요
.and()
.formLogin()
.and()
.httpBasic();
}
}
antMatcher(String antPattern) - Allows configuring the HttpSecurity to only be invoked when matching the provided ant pattern. mvcMatcher(String mvcPattern) - Allows configuring the HttpSecurity to only be invoked when matching the provided Spring MVC pattern.
Generally mvcMatcher is more secure than an antMatcher. As an example:
antMatchers("/secured") matches only the exact /secured URL
mvcMatchers("/secured") matches /secured as well as /secured/, /secured.html, /secured.xyz
List.of(new SimpleGrantedAuthority("ROLE_USER"))
처럼 권한 명칭(ROLE_USER)을 문자열로 지정 RestTemplate ● Blocking I/O 기반의 Synchronous API 즉 비동기적 요소 x / A -> B 순차적 만약 A에 문제가있으면 당연히 B수행안된다. ● RestTemplateAutoConfiguration ● 프로젝트에 spring-web 모듈이 있다면 RestTemplateBuilder를 빈으로 등록해 줍니다.
WebClient ● Non-Blocking I/O 기반의 Asynchronous API ● WebClientAutoConfiguration ● 프로젝트에 spring-webflux 모듈이 있다면 WebClient.Builder를 빈으로 등록해 줍니다.
상황 :
커뮤니티 서비스에서 유저가 올린 이미지는 s3로 업로드 된다.
이때 이미지는 MultipartFile타입으로 전달받아 upload를 하는데
해당 부분에 대한 테스트코드를 작성할 때 MultipartFile에 대한 가짜객체를 어떻게 만들어야할지 고민했었다.
방법:
찾아보니 이를 위한 MockMultipartFile이라는 객체가 org.springframework.mock.web;
에 존재했었다
fun createMockMultipartFile(size:String): MockMultipartFile {
return MockMultipartFile(
"test $size File",
"$size.png",
MediaType.IMAGE_PNG_VALUE,
"$size.png".toByteArray()
)
}
스프링부트가 전반적으로 어떻게 동작해서 편리하게 해주는건지