forDevLife
[TIL]9/13(3일차) 본문
Springboot 2 기초
@ConfigurationProperties 어노테이션 사용중 갑작스러운 빨간알림 발생
-> dependencies { annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" } 해결
- LOMBOK cannot find symbol 에러
- POJO(Plain Old Java Object) : Java EE 같은 특정 프레임워크에 종속적이지 않은 자바 객체
- @ConfigurationProperties : application.yml에서 테스트 프로퍼티 추가 후 prefix를 통해 지정된 클래스에서 프로퍼티를 매핑할 수 있게 한다.
- Spring Boot auto-configuration(자동 환경 설정) : 스프링 부트의 장점이며, 매우 중요한 역할을 함. Web, H2, JDBC를 비롯한 약 100여개의 자동 설정을 제공한다. 새로 추가되는 라이브러리(JAR) 또한 스프링 부트 auto config를 통해 설정이 자동 적용된다.
만약 H2 의존성이 클래스 경로에 존재한다면 자동으로 인메모리 데이터베이스에 접근한다.
-> 이러한 설정은 @EnableAutoConfiguration(@Configuration 함께 사용 필요) 또는 이를 포함하는 @SpringBootApplication 중 하나를 사용하면 된다.
- @SpringBootApplication은 @SprigBootConfiguration + @EnableAutoConfiguration + @ComponentScan의 조합이다.
- @EnableAutoConfiguration
- AutoConfigurationImportSelector.class
- AutoConfigurationImportSelector 클래스는 DeferredImportSelector 인터페이스를 구현한 클래스로 오버라이드 받은 selectImports() 메서드가 자동 설정할 빈을 결정한다.
- 모든 후보 빈을 불러온다. (META-INF/spring.factories에 정의된 자동 설정할 클래스들을 먼저 불러온다.)
* spring.factories : 자동 설정 타깃 클래스 목록. 즉, 이곳에 선언되어 있는 클래스들이 @EnableAutoConfiguration 사용 사 자동 설정 타깃이 된다.
* spring-configuration-metadata.json : 자동 설정에 사용할 프로퍼티 정의 파일이다. 미리 구현되어 있는 자동 설정에 프로퍼티만 주입시켜주면 된다. 따라서 별도의 환경 설정 필요 없다.
* org/springframework/boot/autoconfigure : 미리 구현해놓은 자동 설정 리스트이다. 이름은 '{특정 설정의 이름}AutoConfiguration' 형식으로 되어있다. 예로, H2ConsoleAutoConfiguration과 같다.
- 예를 들어 H2를 자동 설정한다고 가정한다.
1. 먼저 spring.factories에서 자동 설정 대상에 되는지 확인한다.
2. spring-configuration-metadata.json에서 주요 프로퍼티 값들은 무엇이고, 어떤 타입으로 설정할 수 있는지 확인한다.
3. application.properties 혹은 application.yml에서 위를 참고해서 해당 프로퍼티를 직접 변경 가능하다.
spring.h2.console.path=/h2-test
위와 같이 프로퍼티 값을 추가하는 것만으로 자동 환경 설정에 자동으로 적용되어 애플리케이션이 실행된다.
스프링 프로퍼티 문서를 사용해서 더 쉽게 프로퍼티 값을 확인 / 변경할 수 있다.
- 자동 설정 어노테이션 살펴보기
스프링 부트는 자동 설정이 적용되는 조건, 시점 등에 따라 다양한 어노테이션을 지원한다. 이를 통해 설정 관리 능력을 향상시킬 수 있다.
- 자동 설정을 위한 조건 어노테이션 : 조건을 만족하면 자동 설정이 적용된다. (@ConditionalOn~)
- 자동 설정을 위한 순서 어노테이션 : 지정한 특정 자동 설정 클래스들이 적용된 이후 혹은 이전에 해당 자동 설정 적용(@AutoConfigure~)
1. ConditionalOnWebApplication(type = Type.SERVLET) : 웹 어플리케이션일때 적용
2. ConditionalOnClass(WebServlert.class) : WebServlet.class가 클래스 패스에 있을 때
3. ContitionalOnProperty : spring.h2.console.enabled가 true일 때
4. AutoConfigureAfter(~) : 지정한(~) 특정 자동 설정 클래스들이 적용된 후에 해당 자동 설정 적용
5. EnableConfigurationProperties(~) : 자동 설정 프로퍼티가 적용될 때 H2ConsoleProperties 클래스 타입으로 H2 관련 프로퍼티 값을 매핑해서 사용하게 된다.
+ 스프링 프레임워크에서는 일일이 설정했어야 했는데, 부트에서는 이처럼 스프링 개발자가 미리 설정한 방식으로 애플리케이션에 적용하게끔 정의되어 있다.
H2 Console 자동 설정 적용하기 - 자동 설정 이전
1. build.gradle에 의존관계 추가
implementation 'com.h2database:h2'
+ H2는 메모리 데이터 베이스로 보통 테스트 용으로 쓰인다. 주 저장소가 아니기 때문에 불필요하게 컴파일 의존성에 포함될 필요가 없다. 따라서 application.yml로 자동 설정을 진행했다면, 런타임 시점에만 의존하도록 변경해도 된다. -> runtime('com.h2database:h2')
2. 빈을 이용해 H2 콘솔 등록하기
3. console 확인 -> localhost:8080/console -> 콘솔 실행된다.
H2 Console 자동 설정 적용하기 - 자동 설정 이후
다시 한번 metadata.json을 통해 프로퍼티 값을 살펴보자.
spring.h2.console.enabled의 defaultValue가 false로 적용되어 있다. 따라서 해당 프로퍼티만 true로 변경해주면 위와 같이 직접 @Configuration을 통한 빈 등록이 필요가 없어진다.
앞서 진행한 @Configuration 해제 & 의존성을 compile time -> runtime으로 변경 후 yml에 위와 같이 설정해주면 h2 console을 /h2-jh로 매핑해서 사용할 수 있다.
스프링 부트 테스트
스프링 부트에서는 기본적인 테스트 스타터를 제공한다. 스타터에 웬만한 테스트 라이브러리들을 한데 뭉쳐놨기 때문에 편리하게 사용 가능하다. 스타터는 크게 두 가지 모듈로 구성된다.
0. spring-boot-starter-test(아래 두 모듈 함께 사용)
1. spring-boot-test
2. spring-boot-test-autoconfigure(테스트 관련 자동 설정 기능)
스프링 부트 1.4 버전부터는 각종 테스트를 위한 어노테이션 기반 기능을 제공하여, 특정 주제에 맞게 테스트를 구현하고 관리할 수 있다.
@SpringBootTest, @WebMvcTest, @DataJpaTest, @RestClientTest, @JsonTest에 대해서 알아보자.
1. @SpringBootTest
- 통합 테스트를 제공하는 기본적인 스프링 부트 어노테이션이다. 여러 단위 테스트를 하나의 통합된 테스트로 수행할 때 적합하다. 스프링부트 프로젝트를 만들면 메인 클래스와 기본으로 제공된다.
- 실제 구동되는 애플리케이션과 똑같이 애플리케이션 컨텍스트를 로드하여 테스트하기 때문에 하고 싶은 테스트를 모두 수행할 수 있다.
- 단, 애플리케이션에 설정된 모든 빈을 로드하기 때문에 규모가 클 수록 느려진다. 이렇게 되면 단위 테스트라는 의미가 희석된다.
@SpringBootTest 어노테이션 파라미터를 알아보자.
- value : @Value("${value}")를 통해 적용할 프로퍼티를 주입할 수 있다. 테스트 실행 전 발생하며, 기존의 프로퍼티를 오버라이드 한다.
- properties : 테스트가 실행되기 전에 key=value 형식으로 프로퍼티를 추가할 수 있다.
* value / properties를 같이 사용하면 안된다. 위에서는 그냥 보여주기 위한 것
- classes : 애플리케이션 컨텍스트에 로드할 클래스를 지정할 수 있다.
따로 지정 안하면 @SpringBootConfiguration을 찾아서 로드(main 클래스)
- webEnvironment : 애플리케이션이 실행될 때의 웹 환경을 설정할 수 있다. 기본값은 Mock 서블릿을 로드하여 구동되며, 예제에서는 랜덤 포트 값을 주어 구동했다.
(Mock 서블릿 : 개발 환경에 따라 다른 Mock 서블릿 환경의 애플리케이션 컨텍스트를 선택하여 로드되도록 하는 설정 값)
+ profile 환경(개발, QA, 운영 환경)마다 다른 데이터 소스(DB와 서버 간의 연결정보)를 갖는다면 어떻게 할까?
-> @ActiveProfiles("local")과 같은 방식으로 원하는 프로파일 환경 값을 부여할 수 있다.
+ test에서 @Transactional 사용하면 테스트를 마치고 나서 수정된 데이터가 롤백된다. 다만 테스트가 서버의 다른 스레드에서 실행중이면 롤백 발생하지 않는다.
+ @SpringBootTest는 기본적으로 검색 알고리즘을 사용하여 @SpringBootApplication / @SpringBootConfiguration을 찾는다.
2. @WebMvcTest
테스트 스타터에 포함된 자동 설정 패키지인 spring-boot-test-autoconfigure를 사용하면 주제에 따라 가볍게 테스트 가능하다. 주제에 관련된 빈만 애플리케이션 컨텍스트에 로드해서 테스트를 진행한다. (웹 테스트용 : @WebMvcTest)
- 웹에서 테스트하기 힘든 컨트롤러를 테스트하는데 적합하다. 웹상에서 요청과 응답에 대해 테스트를 할 수 있다.
- 시큐리티 혹은 필터까지 자동으로 테스트하며 수동으로 추가 / 삭제가 가능하다.
- MVC 관련 설정인 @Controller, @ControllerAdvice, @JsonComponent, Filter, WebMvcConfigurer, HandlerMethodArgumentResolver만 로드되기때문에 가볍게 테스트가 가능하다.
- @WebMvcTest를 사용하기 위해 테스트 할 특정 컨트롤러 명을 명시해주어야 한다. 주입된 MockMvc는 컨트롤러 테스트 시 모든 의존성을 로드하는 것이 아닌, 특정 컨트롤러 관련 빈만 로드해서 가벼운 MVC 테스트를 수행한다.
- @Service는 @WebMvcTest의 적용 대상이 아니다. 만약 컨트롤러에서 서비스를 사용한다면, @MockMvc를 활용해서 컨트롤러 내부 부 의존성 요소인 service를 가짜 객체로 대체한다. 이를 목 객체라고 한다.
3. @DataJpaTest
- JPA 관련 테스트 설정만 로드한다.
- 데이터 소스의 설정이 정성적인지, JPA를 사용하여 데이터를 제대로 생성, 수정, 삭제하는지 등의 테스트가 가능하다.
- 내장형 데이터베이스를 사용해서 실제 db 사용하지 않고 테스트데이터 베이스로 테스트가 가능하다.
- 기본적으로 인메모리 임베디드 데이터베이스(메인 메모리를 데이터 저장소로 활용)를 사용하여, @Entity 클래스를 스캔해서 Spring Data JPA Repository를 구성한다.
- 별도의 데이터 소스를 사용해서 테스트하려면 다음과 같이 설정한다.
@ActiveProfiles("...")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class JpaTest {
...
}
@AutoConfigureTestDatabase의 기본 설정인
Replace.Any를 사용 -> 내장 데이터베이스 사용
Replace.None을 사용 -> @ActiveProfiles에 설정한 프로파일 환경값에 따라 데이터 소스가 적용된다.
+ application.yml에서 프로퍼티 설정을 spring.test.database.replace: NONE으로 설정도 가능하다.
- @DataJpaTest는 JPA 테스트 끝날 때마다 자동으로 테스트에 사용한 데이터를 롤백한다. 따라서 테스트 후 실제 데이터가 변경되었는지 걱정할 필요 없다.
4. @RestClientTest
- REST 통신의 데이터형으로 사용되는 JSON 형식이 예상대로 응답을 반환하는지 등을 테스트할 수 있다.
- MockRestServiceServer : 클라이언트와 서버 사이의 REST 테스트를 위한 객체이다. 내부에서 restTemplate을 바인딩하여 실제로 통신이 이루어지게끔 구성할 수도 있다. 여기에서는 목 객체와 같이 통신이 이루어지진 않지만, 지정한 경로에 예상되는 반환값 혹은 에러를 반환하도록 명시하여 간단하게 테스트를 진행했다.
5. @JsonTest
- JSON의 직렬화 / 역직렬화를 수행하는 라이브러리인 Gson, Jackson API의 테스트를 제공한다.
- 각각 GsonTester, JacksonTester를 사용하여 테스트를 수행한다.
- JSON 테스트는 다음과 같이 두 가지로 나눌 수 있다.
-. 문자열로 나열된 Json 데이터를 객체로 변환하여 반환된 객체값을 테스트
-. 객체를 문자열로 변환하여 테스트
6. 정리
스프링 부트의 테스트 어노테이션은 Junit 자체에 내장된 테스트 메서드를 스프링에서 사용하기 편하도록 가공한 것이다.
스프링의 모든 빈을 올리는 대신, 각 테스트에 필요한 가짜 객체를 만들어 테스트 하는 방법을 사용해보자.
스프링 부트 웹
스프링 부트 웹을 이용해서 게시판을 만들어본다. 웹은 주로 뷰 페이지나 API 서비스의 구현에 사용된다. API는 '스프링 부트 데이터 레스트'를 사용해서 다루고, 이번에는 뷰 페이지를 만드는 법을 다룬다.
- 커뮤니티 게시판 설계하기
- 커뮤니티 게시판 프로젝트 준비하기
- 커뮤니티 게시판 구현하기
1. 커뮤니티 게시판 설계
1) 클라이언트 -> 서버 데이터 요청
2) 컨트롤러 -> 서비스 -> 리포지토리 -> DB를 거쳐 데이터가 돌아오고, 다시 컨트롤러에서 타임리프 뷰를 호출하여 브라우저에 반환한다.
간단한 CRUD 기능만 제공하는 게시판을 구현하도록 한다.
2. 게시판 구현하기
2.1 구현 요구 사항
1. 회원 기능 없음(로그인할 때 권한 인증 / 접근등의 권한 부여기능) -> 이후에 배우도록 하고, 우선 내용 위주로 진행한다.
2. Board(게시판), User(회원) 엔티티 및 테이블을 만든다. 회원의 pk를 게시판에서 FK로 사용하고, OneToOne mapping을 진행한다.
2.2 구현 순서
1. 프로젝트 의존성 구성
2. 스프링 부트 웹 스타터 살펴보기
3. 도메인 매핑하기
4. 도메인 테스트 하기
5. CommandLineRunner 이용해서 DB에 데이터 넣기
6. 게시글 리스트 기능 만들기
7. 타임리프 자바 8 날짜 포맷 라이브러리 추가
8. 페이징 처리
9. 작성 폼 만들기
2.2.1 프로젝트 의존성 구성
plugins : 필요한 플러그인을 적용한다.(기본으로 되어있는거 그대로 사용)
dependencies : 프로젝트 내에 사용할 라이브러리의 의존성을 설정한다. 메인 부트 버전에 맞는 호환성을 가져오기 위해 디폴트 버전 사용한다.
2.2.2 스프링 부트 웹 스타터 살펴보기
Spring-boot-starter는 다음 의존성을 제공했었다.
- spring-boot : 스프링 부트 기존 제공 의존성
- spring-boot-autoconfigure : 스프링 부트 자동 환경 구성에 필요한 의존성
- spring-boot-starter-logging : 각종 로그를 사용하는데 필요한 의존성
- javax.annotation-api : 소프트웨어의 결함을 탐지하는 어노테이션을 지원하는 의존성
- spring-core : 스프링 코어 사용시 필요한 의존성
- snakeyaml : yaml을 사용하는 데 필요한 의존성
Spring-boot-web-starter는 다음과 같은 의존성을 제공한다.
- spring-boot-starter : 위의 6가지 포함
- spring-boot-starter-tomcat : 내장 톰캣 사용 위한 스타터
- hibernate-validator : 어노테이션 기반의 표준화된 제약 조건 및 유효성 검사 규칙을 표현하는 라이브러리
- spring-boot-starter-json : jaskson 라이브러리 지원하는 starter
- spring-web : Http integration, servlet filters, spring Http invoker 및 Http core를 포함시킨 라이브러리
- spring-webmvc : request를 전달하는 MVC로 디자인 된 DispatcherServlet 기반의 라이브러리
2.2.3 도메인 매핑하기
뷰에 바인딩하여 반환하는 흐름을 알아본다.
도메인 매핑은 JPA를 사용해서 DB와 도메인 클래스를 연결해주는 작업이다. DB에서 (도메인을 활용하여) 리포지토리까지의 데이터 처리 흐름은 다음과 같다.
* 리포지토리 : 스프링이 관리하는 컴포넌트에서 퍼시스턴스 계층에 대해 더 명확하게 명시하는 특수 제네릭 스테레오 타입이다.
(퍼시스턴스 : 물리적 저장 공간. 영속성을 가진 파일이나 DB에 로직을 구현하는 것을 의미하기도 한다.)
Board / User를 각각 Serializable을 implements하여 구현한다. (Builder 패턴 이용)
2.2.4 도메인 테스트 하기
- 앞서 배운 @DataJpaTest를 사용해서 도메인 테스트를 진행한다. 테스트 후 자동 롤백으로 DB에 반영되지 않는다.
@ExtendWith(SpringExtension.class)
@DataJpaTest
public class JpaMappingTest {
private final String boardTestTitle = "테스트";
private final String email = "test@gmail.com";
@Autowired
UserRepository userRepository;
@Autowired
BoardRepository boardRepository;
@BeforeEach
public void init() {
User user = userRepository.save(User.builder()
.name("havi")
.password("test")
.email(email)
.createDate(LocalDateTime.now())
.build());
boardRepository.save(Board.builder()
.titie(boardTestTitle)
.subTitle("서브 타이틀")
.content("콘텐츠")
.boardType(BoardType.free)
.createdDate(LocalDateTime.now())
.updatedDate(LocalDateTime.now())
.user(user).build()
);
}
@Test
public void 정상_생성_테스트() {
User user = userRepository.findByEmail(email);
assertThat(user.getName()).isEqualTo("havi");
assertThat(user.getPassword()).isEqualTo("test");
assertThat(user.getEmail()).isEqualTo(email);
Board board = boardRepository.findByUser(user);
assertThat(board.getTitie()).isEqualTo(boardTestTitle);
assertThat(board.getSubTitle()).isEqualTo("서브 타이틀");
assertThat(board.getBoardType()).isEqualTo(BoardType.free);
}
}
- ExtendWith를 통해 JUnit에 내장된 러너가 아닌 어노테이션에 정의된 클래스를 호출한다. 또한 JUnit의 확장 기능을 지정하여 각 테스트 시 독립적인 애플리케이션 컨텍스트를 보장한다.
* 애플리케이션 컨텍스트 : 빈의 생성과 관계 설정 같은 제어를 담당하는 IOC 객체를 빈 팩토리라 부르며, 이를 더 확장한 개념이다.
BoardService 생성
@Service
public class BoardService {
private final BoardRepository boardRepository;
public BoardService(BoardRepository boardRepository) {
this.boardRepository = boardRepository;
}
public Page<Board> findBoardList(Pageable pageable) {
pageable = PageRequest.of(pageable.getPageNumber() <= 0 ? 0 : pageable.getPageNumber() - 1, pageable.getPageSize());
return boardRepository.findAll(pageable);
}
public Board findBoardByIdx(Long idx) {
return boardRepository.findById(idx).orElse(new Board());
}
}
- findBoardList :
pageable로 넘어온 pageNumber 객체가 0 이하일 때 0으로 초기화. 기본 페이지 크기인 10으로 새로운 PageRequest 객체를 만들어 페이징 처리된 게시글 리스트를 반환한다.
- findBoardByIdx :
board의 idx 값을 사용하여 board 객체를 반환한다.
BoardController 생성
@Controller
@RequestMapping("/board")
public class BoardController {
@Autowired
BoardService boardService;
@GetMapping({"", "/"})
public String board(@RequestParam(value = "idx", defaultValue = "0") Long idx, Model model) {
model.addAttribute("board", boardService.findBoardByIdx(idx));
return "/board/form";
}
@GetMapping("/list")
public String list(@PageableDefault Pageable pageable, Model model) { // pageableDefault 어노테이션의 파라미터인 size, sort 등을 이용해서 페이징 처리에 대한 규약을 정의한다.
model.addAttribute("boardList", boardService.findBoardList(pageable));
return "/board/list";
}
}
Pageable 객체를 생성하기 위해서 사용되는 요청 파라미터
- page : 가져올 페이지 (기본값 : 0)
- size : 페이지의 크기 (기본값 : 20)
- sort : 정렬 기준으로 사용할 속성으로 기본적으로 오름차순으로 한다. 정렬 기준 속성이 2개 이상인 경우에는 sort 파라미터를 2개 이상 넣어주면된다. 예) sort=firstname&sort=lastname,asc.
- 간단히 알아보면, 위와 같은 쿼리 파라미터를 기반으로 pageable 객체를 생성 & 서비스 메서드로 전달한다.
서비스 메서드에서는 전달받은 Pageable 객체를 대상으로 페이지 / 크기 / 정렬 기준 등의 정보를 추출한다.
PageRequest.of에서, PageRequest는 pageable을 상속받는 클래스이다. of를 통해 위에서는 새로운 unsorted 된 PageRequest를 생성해서 repository로 넘겨준다.
boardRepository에서는 전달받은 Pageable 객체를 대상으로 findAll 메서드를 실행 & 페이징 처리 된 Page를 반환한다.
(*Page : Object list의 sublist)
2.2.5 CommandLineRunner를 사용하여 DB에 데이터 넣기
- 애플리케이션 구동 후 CommandLineRunner로 테스트용 데이터를 DB에 넣을 수 있다. 이 인터페이스는 애플리케이션 구동 후 특정 코드를 실행시키고 싶을 때 직접 구현하는 인터페이스이다.
- 구동 시 테스트 데이터를 함께 생성하여 데모 프로젝트를 실행 / 테스트하고 싶을 때 편리하다.
- 또한 여러 CommandLineRunner를 구현하여 같은 애플리케이션 컨텍스트의 빈으로 사용할 수 있다.
한 명의 회원을 생성 & 그 회원의 글 200개를 작성하는 쿼리를 생성해보자.
@SpringBootApplication
public class SpringBootCommunityWebApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootCommunityWebApplication.class, args);
}
@Bean // 스프링은 빈으로 생성된 메서드에 파라미터로 DI 시키는 메커니즘이 존재한다. 생성자를 통해 의존성을 주입시키는 방법과 유사하다. 이를 이용해서 CommandLineRunner를 빈으로 등록 후, 두 repo를 주입 받는다.
public CommandLineRunner runner(UserRepository userRepository,
BoardRepository boardRepository) throws Exception {
return (args -> {
// 메서드 내부에 실행이 필요한 코드를 작성한다. User 객체를 빌더 패턴을 사용해서 생성한 후, 저장한다.
User user = userRepository.save(User.builder()
.name("havi")
.password("test")
.email("havi@gmain.com")
.createDate(LocalDateTime.now())
.build());
// 페이징 처리 테스트를 위해 빌더 패턴을 이용한다. IntStream의 rangeClosed를 사용해서 index 순서대로 Board 객체 200개를 생성해서 저장한다.
IntStream.rangeClosed(1, 200).forEach(index ->
boardRepository.save(Board.builder()
.titie("게시글" + index)
.subTitle("순서" + index)
.content("콘텐츠")
.boardType(BoardType.free)
.createdDate(LocalDateTime.now())
.updatedDate(LocalDateTime.now())
.user(user)
.build()
));
});
}
}
- CommandLineRunner는 어떤 방법을 사용하든 빈으로 등록해야 한다!
2.2.6 게시글 리스트 기능 만들기 ~ 2.2.8 페이징 처리
- boardList.first가 해당 페이지의 처음이라면 아무것도 노출 안함(display:none)
- 처음이 아니라면 /board/list?page=boardList.number의 링크를 제공한다.
- « : <<
- &lsaquo : <
- &rsaquo : >
- &» : >>
page와 boardList.number + 1이 동일하면 class에 active를 할당해서 강조되도록 한다.
2.2.9 작성 폼 만들기
$(...?) 처럼 구문 뒤에 "?"를 붙여서 null 체크를 추가할 수 있다. 값이 null인 경우에는 빈 값이 출력되도록 할 수 있다.
'각종 회고' 카테고리의 다른 글
[TIL]9/15 - AOP (0) | 2021.09.29 |
---|---|
[TIL]9/15(5일차) - WEB2 : OAuth 2.0 (0) | 2021.09.15 |
[TIL]9/14(4일차) - Spring Boot Security + OAuth (0) | 2021.09.14 |
[TIL] 9/10 (2일차) (0) | 2021.09.10 |
[TIL] 9/9 (1일차) (0) | 2021.09.09 |