노력과 삽질 퇴적물

스프링부트: 기초 및 입문 (2) 본문

프로그래밍note/언어. JAVA & JDK 계열

스프링부트: 기초 및 입문 (2)

MTG 2023. 4. 9. 16:43
목차
 필요한 파일
 환경설정
 라이브러리 연동
 구동 및 배포
기초 및 입문 (2)
 기초 이론
 제어
 모델
 뷰
 단위 테스트
기초 및 입문 (3)
 ???


* 자바 기본 문법을 알고 있다는 전제하에 작성했습니다.





1. 기초 이론
 
* 필요한 기능 혹은 구현중 발생하는 이슈를 해결하기 위해 스프링부트내에서 직간접으로 쓰이는 용어를 알아야 검색이 용이하므로 기본적인 구조 및 빈번하게 엮이는 개념만 추렸습니다.
 

1) MVC 패턴

-> 안드로이드 진저브레드~허니콤 시절(...)에는 MVC패턴은 옛날 고릿적 물건이여서 이런게 있다정도만 짚어도 됩니다였지만, 스프링부트에서는 아니더군요.
-> 발전사.
 1979년에 최초로 소개된 이래 파생 패턴이 여러 갈래여도 2002년에 W3C에서 웹어플리케이션 표준 구조에 포함되게 투표로 정했을뿐 아니라 XHTML 2.0규격에도 통합될 예정이라 합니다. 즉, 나온지 오래됐어도 앞으로 오래 사용될 디자인 패턴 유력후보이고, 기존에도 많이 쓰이고 있는터라 파악해둘 필요성이 높습니다.
 참고로 MVC패턴은 어플리케이션 개발에 대한 패턴을 말하는것으로 '웹 어플리케이션'에만 한정되는것이 아니니 주의해야합니다.
 
[출처: 일반 웹 애플리케이션에서 MVC 패턴. 한국데이터산업진흥원]
-> 패턴 구성원.
 Model(모델): 비즈니스 로직&데이터 모듈. 사용자를 위해 '무엇'을 수행. 표시 형식에 의존X. 순수하게 public함수로 이뤄진다.
 View(뷰): UI모듈(or레이어). 사용자를 위해 '보여주기'를 수행. 반드시 모델과 상호 작용하여 사용자에게 갱신된 값등을 보여준다.
 Controller(컨트롤러): 제어모듈. 사용자를 위해 '어떻게'를 수행. 입력을 받고, 입력에 맞는 모델/뷰를 호출하여 응답을 보낸다.
-> 설계원칙.
 '관심사의 분리'(Seperation of Concerns, SoC)에 맞춰 데이터 처리/출력등 동작에 맞게 모듈로 묶되 모듈간 느슨한 연결이 존재함으로써 개발 프로세스상 프로그래머와 디자이너가 각자의 영역에서 작업이 가능하다.
 또한 백엔드 사이드에서는 출력되는 클라이언트가 다각화되어도 통신만 맞추면 하나의 서버로 여러 플랫폼과 대응이 가능하다.
 
 
2) DTO/? DAO?
[출처: 흔한 개발자의 개발 노트]
-> DB에서 직접 읽고/쓰면 DAO, 모델~컨트롤러를 오가면 DTO입니다. 처리 과정상, DTO가 뷰에 가깝고 DB상 1개 이상의 테이블 읽기/쓰기에 사용하는 데이터 객체 하나를 공용으로 쓰는건 좋지 않습니다.
 가령 DAO내 @CreationTimestamp@UpdateTimestamp를 적용된 필드가 DB에 입력하는 함수 이후 해당 DAO에도 반영되니 이 객체를 그냥 다른데서 공용으로 쓰다간 원치 않는 동작으로 이어지겠죠?
 

3) 어노테이션(혹은 애너테이션)
-> @Deprecated@Overrided처럼 클래스/함수/함수인자/멤버 변수등에 붙여서 쓴걸 본적이 있으실겁니다 혹은 쓰는 경우가 있습니다. 롬북(Lombok)을 통한 생산성 향상과 엮여있긴 한데, 자바쪽 강점중 하나라고 얼핏 들리는걸 보면 맞나 싶기도 하지만 기본 이론만 짚겠습니다.
-> 위키백과 설명은 "메타데이터의 일종이다. 보통 @ 기호를 앞에 붙여서 사용한다. JDK 1.5 버전 이상에서 사용 가능하다. 자바 애너테이션은 클래스 파일에 임베디드되어 컴파일러에 의해 생성된 후 자바 가상머신에 포함되어 작동"이고, nextree 블로그를 참조하면, "비즈니스 로직에는 영향을 주지는 않지만, 해당 타겟의 연결 방법이나 소스코드의 구조를 변경할 수 있습니다."로 나옵니다.
 가령 Getter/Setter 메소드(혹은 함수)가 필요하면 프로그래머가 이를 작성하거나 이클립스등의 IDE에서 해당 코드를 생성시켜주는 기능을 써도 되지만, 클래스 블록 위에 @Getter/ @Setter를 입력해두면 클래스 멤버 변수를 추가하고서 Getter/Setter 메소드를 작성하지 않아도 됩니다. 편리하죠. 게다가 JSON이나 RSS xml등에서 적용해두면, 클래스 멤버 변수명과 JSON/RSS xml의 변수의 네이밍룰을 다르게 해야 할 경우에도 용이하더군요.


4) bean과 의존성 주입(Dependency Injection)
-> 싱글톤 패턴은 특정 매니저 클래스에 static변수를 사용한 디자인 패턴입니다. 스프링부트에서는 bean객체들을 싱글톤 방식으로 관리하는걸로 알고 있습니다.
 XxxManager.getInstance().getXXX()랑 조금 다르게 bean으로 등록할 클래스 블록위에 @Component/@Service/@Repository등을 명시하고, bean으로 등록한 클래스를 호출할 곳에서 @Autowired를 명시해서 쓰면 됩니다.
-> bean객체라는 용어와 별개로 @Bean은 메소드에 사용한다고 합니다.
-> Junit을 위한 테스트 작성시 클래스 멤버 변수를 주입해두면(=필드 주입) Field injection is not recommended가 종종 발생하곤 했는데, private니 외부에서는 접근이 안 되니 테스트 코드에서 실행해봐야... 여러 상황상 '생성자 주입'이 지체가 될 여지가 적습니다.
-> 세부적인 종류만 정리해두자면, 생성자 주입/수정자 주입/메소드 주입/필드 주입
 

5) RESTful API
-> HTTP를 사용해 구현이 가능한데, 일반적인 HTTP 메서드를 쓰는걸 RESTful API로 혼동해온터라 이번 기회에 개요라도 정리해봅니다. 
-> REST API? RESTful API?
 "REST 아키텍처 스타일을 따르는 API를 REST API라고 합니다. REST 아키텍처를 구현하는 웹 서비스를 RESTful 웹 서비스라고 합니다. RESTful API라는 용어는 일반적으로 RESTful 웹 API를 나타냅니다. 하지만 REST API와 RESTful API라는 용어는 같은 의미로 사용할 수 있습니다."
-> REST 아키텍처의 6 원칙(six guiding constraints).
ⓐ 균일한 인터페이스(Uniform interface): 서버가 표준 형식으로 정보를 전송. 서버에 있는 리소스 형식과 전송하는 리소스의 형식이 다를 수 있음.
ⓑ 무상태(Statelessness): 서버에 요청이 들어온 클라이언트의 콘텐스트가 서버에 저장X
ⓒ 계층화 시스템(Layered system): 클라이언트의 요청을 처리하기 위해 보안부터 비즈니스 로직등 여러 서버에서 실행이 되도록 설계가 가능하고, 서버가 다른 서버로 전달할 수도 있다.
ⓓ 캐시 가능성(Cacheability)/캐시 처리 가능(Cacheable): 서버 응답시간 개선을 위해 클라이언트가 응답(일부)을 캐싱.
 [예. 웹페이지에서는 머릿글/바닥글처럼 공통되거나 동일한 이미지를 클라에 캐싱or저장해서 쓴다거나 게임의 경우 아이템or재화 변동없이 메뉴만 이동하면 아이템or재화에 대한 값은 캐싱해두걸로 출력.]
ⓔ 클라이언트/서버 구조: 아키텍쳐를 단순하고 작은 단위로 분리시켜 클라-서버의 각 파트를 간결성/확장성있게.
 [원문: Separation also simplifies the server components, improving scalability, but more importantly it allows components to evolve independently]
ⓕ 온디맨드 코드(Code on demand): (optional) 자바스크립트등으로 서버가 클라이언트에 관여하는 로직을 전송.
  예: 회원가입시 잘못된 입력 양식 체크에 대해 서버가 판단 및 제어코드 전송.
->그리고 아마존 문서를 보면, 여태 서버내 처리 성공에 대한 기본적인 statusCode가 200이고 delete할때나 20X로 써오고 작업해왔는데 POST메서드는 201이라 나오네요.


5) 빌더 패턴과 Lombok의 @Builder
-> 부모 클래스에서 @Getter, @AllArgsConstructor를 주고 자식 클래스내 생성자에서 @Builder를 쓰는걸로 나옵니다.





2. 제어
 
1) 컨트롤러
-> @Controller? @RestController?
 화면 생성이 아닌 JSON/XML같은 리스폰스를 내려주려면 @ResponseBody를 기본으로 처리하는 @RestController를 쓰는쪽이 수월합니다.
-> HttpStatus.
 아파치쪽을 쓰기에는 스프링 프레임워크쪽 enum이 상태 코드값과 메시지를 다 쓸 수 있어서 앞으로 스프링 기준으로 하겠습니다.

 
2) 예제, 최소 구현 버전
-> 포스트맨으로 컨트롤러를 호출해보겠습니다.
@Slf4j
@RestController
@RequestMapping("/simple")
public class SimpleController
{
	@RequestMapping(method=RequestMethod.GET)
	public String doGet()
	{
		log.debug( "GET called" );
		return "this is simple GET api";
	}
	
	@RequestMapping(method=RequestMethod.POST)
	public String doPost()
	{
		log.debug( "POST called" );
		return "this is simple POST api";
	}

	@RequestMapping(method=RequestMethod.PUT)
	public String doPut()
	{
		log.debug( "PUT called" );
		return "this is simple PUT api";
	}

	@RequestMapping(method=RequestMethod.DELETE)
	public String doDelete()
	{
		log.debug( "DELETE called" );
		return "this is simple DELETE api";
	}
}
보다시피 컨트롤러 하나와 주소 하나에 다수의 매소드 사용이 가능합니다. 모델과 뷰 모델에 대한 간단한 예시까지 브라우저로 주소만 입력하면 되는 RequestMethod.GET으로 진행하겠습니다.


 
 

3. 모델
 
1) 비즈니즈 로직과 데이터.
-> 1. 기초 이론의 'DTO/? DAO?'에 정리한 MVC구조상,
@Service를 적용하는 클래스: 비즈니스 로직 전담.
@Repository를 적용하는 클래스: DB제어 쿼리문을 직접 입력치않고도 자바 코드로만으로도 처리 가능.
@Entity를 적용하는 클래스: DB에 입력할 값이나 읽어온 값이 들어있는 DAO.
 
 
2) 예제, 최소 구현 버전
-> 이번에도 구조 이해를 위한 최소한의 코드지만, 작성해야 할 부분이 좀 많아서 순서대로 따라하시면 됩니다.
① DB, 테이블 생성: mariaDB 컬럼명 기본 설정은 스네이크형 입니다.(수정가능)
CREATE TABLE `simple` (
	`index_no` BIGINT(20) NOT NULL AUTO_INCREMENT,
	`memo` VARCHAR(256) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
	`created_date` DATETIME NULL DEFAULT NULL,
	`modified_date` DATETIME NULL DEFAULT NULL,
	`created_timestamp` TIMESTAMP NULL DEFAULT NULL,
	`updated_timestamp` TIMESTAMP NULL DEFAULT NULL,
	PRIMARY KEY (`index_no`) USING BTREE
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
;
② Repository(리포지토리)
... ... ...
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface SimpleRepository extends JpaRepository<SimpleEntity, Long>
{

}
 
③ DAO와 DTO: @CreatedDate, @LastModifiedDate를 원치 않으시면 아래 주석을 참조해서 코드에서 지우시면 됩니다.
... ... ...
import java.sql.Timestamp;
import java.time.LocalDateTime;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;//@CreatedDate, @LastModifiedDate
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;//@CreatedDate, @LastModifiedDate
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "simple")
@EntityListeners(AuditingEntityListener.class)//@CreatedDate, @LastModifiedDate
public class SimpleEntity
{
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long index_no;//반드시 jakarta.persistence.Id로
	
	@Column(nullable = true)
	private String memo;
	
	@CreatedDate
	@Column(updatable = false)
	private LocalDateTime created_date;

	@LastModifiedDate
	@Column(updatable = true)
	private LocalDateTime modified_date;
	
	@CreationTimestamp
	@Column(updatable = false)
	private Timestamp created_timestamp;
	
	@UpdateTimestamp
	@Column(updatable = true)
	 private Timestamp updated_timestamp;
}

... ... ...
import lombok.Data;

@Data
public class SimpleDto
{
	private long indexNo;
	private String memo;
	private long updatedTimeStamp;
}
 
④ Service(서비스)
... ... ...
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class SimpleService
{
	private final SimpleRepository repo;
	
	@Autowired
	public SimpleService(SimpleRepository arg)
	{//생성자 기반 객체 주입
		this.repo=arg;
	}
	
	@Transactional
	public long save(SimpleDto argDto, boolean isNew)
	{
		long result = -1;
		
		try {
			SimpleEntity dao = new SimpleEntity();
			dao.setMemo( argDto.getMemo() );
			
			if(isNew==false)	{	dao.setIndex_no( argDto.getIndexNo() );	}
			
			SimpleEntity resultEntity = repo.saveAndFlush( dao );//Transactional에 세트격으로
			result = resultEntity.getIndex_no();
		} catch (IllegalArgumentException e) {
		}
		return result;
	}
	
	public SimpleDto find(long argNo)
	{
		SimpleDto result = new SimpleDto();
		result.setIndexNo( -1L );
		
		try {
			Optional<SimpleEntity> optional = repo.findById( argNo );
			if(optional!=null && optional.isPresent())
			{
				SimpleEntity dao = optional.get();
				result.setIndexNo( dao.getIndex_no() );
				result.setMemo( dao.getMemo() );
				result.setUpdatedTimeStamp( dao.getUpdated_timestamp().getTime() );
			}
		} catch (IllegalArgumentException e) {
		}
		
		return result;
	}
}
 
⑤ 모델 제어를 위한 컨트롤러 수정
... ... ...
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequestMapping("/simple")
public class SimpleController
{
	private final SimpleService service;

	@Autowired
	public SimpleController(SimpleService service)
	{
		this.service = service;
	}
	
	@RequestMapping(method=RequestMethod.GET)
	public String doGet()
	{
		log.debug( "doGet called" );
		
		long lastNo = -1;
		SimpleDto newDto = null;
		for(int idx=0; idx<10; idx++)
		{
			newDto = new SimpleDto();
			newDto.setMemo( "test save idx="+Integer.toString( idx ) );
			lastNo = service.save( newDto, true );
		}
		
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		
		
		SimpleDto dtoFromService = service.find( lastNo );
		dtoFromService.setMemo( "update Test in controller" );
		service.save( dtoFromService, false );
		
		dtoFromService = null;
		dtoFromService = service.find( lastNo );
		
		return "this is simple GET api" + "\n memo:"+dtoFromService.getMemo() + " (" + Long.toString( dtoFromService.getUpdatedTimeStamp() ) + ")";
	}
... ... ...
 
⑥ (선택사항) 어플리케이션 클래스
-> 정확히는 DAO인 SimpleEntity내 @CreatedDate, @LastModifiedDate 사용에 필요한 과정입니다. 만약 해당 어노테이션를 쓰지 않으면 생략이 가능합니다.
... ... ...
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@SpringBootApplication
@EnableJpaAuditing	//@CreatedDate, @LastModifiedDate
public class InterlockingApplication
{
... ... ...
[#Epoch Converter]로 변환하면,
Your time zone: 2023년 4월 9일 일요일 오후 3:24:38.554 GMT+09:00로 나옵니다.
이제 DB를 확인해보겠습니다.
코드상 마지막 행을 수정했습니다.
index_no가 10인 행의 memo, modified_date, updated_timestamp를 확인하면 DB값으로 리스폰스된게 확인됩니다.





4. 뷰
 
* 템플릿 엔진은 안 쓰려고 했는데, 내부 모니터링용 페이지 1개는 필요해서 그걸 기준으로 작성하고, 다른 API들은 RSS나 json등으로 프론트에서 처리가능한 형태로 응답 처리하려 합니다.
 
1) 템플릿 설정
-> pom.xml과 application.properties를 수정합니다.
... ... ...
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-thymeleaf</artifactId>
	</dependency>
... ... ...
 
... ... ...
spring.thymeleaf.prefix=classpath:/view-templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.check-template-location=false
... ... ...
 
 
2) html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
	<head>
	</head>
	<body>
		<div>
			<p class="text" th:text="${timeStampFromController}"></p>
		</div>
	</body>
</html>
 
 
3) 뷰를 위한 컨트롤러.
@Slf4j
@RestController
@RequestMapping("/simple")
public class SimpleController
{
	private final SpringTemplateEngine templateEngine;
	private final SimpleService service;

	@Autowired
	public SimpleController(SpringTemplateEngine templateEngine, SimpleService service)
	{
		this.templateEngine = templateEngine;
		this.service = service;
	}
	
... ... ...
	@RequestMapping(method=RequestMethod.GET, path = "/view", produces = MediaType.TEXT_HTML_VALUE)
	public String doGetView()
	{
		Context result = new Context();
		
		result.setVariable("timeStampFromController", System.currentTimeMillis());
		
		return templateEngine.process("simpleView", result);
	}
... ... ...
 


 
 
 
5. 단위 테스트
 
* Junit5기준입니다.
* 단위 테스트전 [TRUNCATE `simple`;]로 테이블 초기화를 했습니다.
 
1) pom.xml
-> 이전 포스팅인 '스프링부트: 기초 및 입문 (1)'에 입력해둔 프로젝트 생성 URL에 이미 처리된 상태겠지만, xml상 아래와 같이 입력되어 있습니다.
... ... ...
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
... ... ...
 
 
2) 예시
-> 깃허브가 아직 정비중이라 불가피하게 코드 전체 입력입니다. 그런데 149줄인지라...
package com.analoggreen.interlocking.simple;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.Optional;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import lombok.extern.slf4j.Slf4j;


@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@AutoConfigureMockMvc
@SpringBootTest
public class SimpMvcTester
{
	private interface OrderInterface
	{
		int init = 1;
		int repository = 2;
		int service = 3;
		int controller = 4;
	}
	
	@Autowired
	private MockMvc mvc;
	@Autowired
	private SimpleRepository repo;
	@Autowired
	private SimpleService service;

	@Order(OrderInterface.init)
	@Test
	public void init()
	{
		if(repo.count()>0)	{	repo.deleteAll();		}
	 }

	@Order(OrderInterface.repository)
	@Test
	public void TestRepository()
     {
		String TEST_MSG = "TestRepository_func";
		int LOOP_SIZE = 3;
		SimpleEntity insertEntity = null;

		for(int idx=0; idx<LOOP_SIZE; idx++)
		{
			insertEntity = new SimpleEntity();
			insertEntity.setMemo( TEST_MSG );
			assertThat( insertEntity.getIndex_no() ).isNull();
			repo.save( insertEntity );
			
			assertThat( insertEntity.getIndex_no() ).isNotNull();
			assertThat( insertEntity.getIndex_no() ).isGreaterThan( 0 );
		}
		assertThat( repo.count() ).isGreaterThanOrEqualTo( LOOP_SIZE );
		log.debug( "(TestRepository)repo.count()={}", repo.count() );
		
		Optional<SimpleEntity> opt = repo.findById( insertEntity.getIndex_no() );
		SimpleEntity entityFromTable = opt.get( );
		assertThat( entityFromTable.getIndex_no() ).isEqualTo( insertEntity.getIndex_no() );
		assertThat( entityFromTable.getCreated_timestamp() ).isNotNull();
		assertThat( entityFromTable.getCreated_timestamp().getTime() ).isLessThan( System.currentTimeMillis() );
		assertThat( entityFromTable.getCreated_timestamp().getTime() ).isEqualByComparingTo( entityFromTable.getUpdated_timestamp().getTime() );
     }
	
	@Order(OrderInterface.service)
	@Test
	public void TestService()
     {
		String TEST_MSG = "TestService_func";
		int LOOP_SIZE = 3;
		SimpleDto insertDto = null;
		long lastNo = -1;

		for(int idx=0; idx<LOOP_SIZE; idx++)
		{
			insertDto = new SimpleDto();
			insertDto.setMemo( TEST_MSG );
			lastNo = service.save( insertDto, true );
			
			assertThat( lastNo ).isNotIn( -1 );
		}
		assertThat( repo.count() ).isGreaterThan( LOOP_SIZE );
		log.debug( "(TestService)repo.count()={}", repo.count() );

		SimpleDto dtoFromTable = new SimpleDto();
		dtoFromTable = service.find( lastNo );
		dtoFromTable.setMemo( "UPDATE TEST TestService()" );
		final long beforeTimeStamp = new Long(dtoFromTable.getUpdatedTimeStamp());
		log.debug( "(TestService)beforeTimeStamp={}", beforeTimeStamp );
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
		}
		assertThat( service.save( dtoFromTable, false ) ).isNotIn( -1 );
		
		dtoFromTable = service.find( lastNo );
		assertThat( dtoFromTable.getMemo() ).isNotEqualTo( TEST_MSG );
		assertThat( beforeTimeStamp ).isNotEqualByComparingTo( dtoFromTable.getUpdatedTimeStamp() );

		log.info( "TestService.FIN" );
     }
	
     @Order(OrderInterface.controller)
     @Test
     public void TestController() throws Exception
     {
		String URL = "/simple";
		MvcResult mvcResult = null;
		String responseBosy = null;

		mvcResult = mvc.perform(MockMvcRequestBuilders.get(URL).accept(MediaType.TEXT_PLAIN))
							.andDo(print()).andExpect(status().isOk()).andReturn();
		responseBosy = mvcResult.getResponse().getContentAsString();
		assertThat( responseBosy.toLowerCase() ).contains( "simple get api" );
		
		mvcResult = mvc.perform(MockMvcRequestBuilders.post(URL).accept(MediaType.TEXT_PLAIN))
						      .andDo(print()) .andExpect(status().isOk()).andReturn();
		responseBosy = mvcResult.getResponse().getContentAsString();
		assertThat( responseBosy.toLowerCase() ).contains( "simple post api" );
		
		mvcResult = mvc.perform(MockMvcRequestBuilders.put(URL).accept(MediaType.TEXT_PLAIN))
						      .andDo(print()).andExpect(status().isOk()).andReturn();
		responseBosy = mvcResult.getResponse().getContentAsString();
		assertThat( responseBosy.toLowerCase() ).contains( "simple put api" );
		
		mvcResult = mvc.perform(MockMvcRequestBuilders.delete(URL).accept(MediaType.TEXT_PLAIN))
						      .andDo(print()) .andExpect(status().isOk()).andReturn();
		responseBosy = mvcResult.getResponse().getContentAsString();
		assertThat( responseBosy.toLowerCase() ).contains( "simple delete api" );

		log.info( "TestController.FIN" );
     }
}


 
 
 
기타. 참조자료

1) MVC 이론
1-1) 국문
 
 
2) 스프링부트 기반
2-1) 국문
 
 
2-2) 영문
 
 
 



기타. 변경이력

일자
 변경이력
 2023-04-09  초안 작성. [#blogger][#티스토리]