스프링 부트 OAuth 구현을 하다가 순환참조 이슈가 발생하였다. 이 순환참조 이슈는 Spring Boot 2.6x 버전부터 발생한다고 한다. 앞으로 프로젝트 할 때 흔히 겪을 수 있는 문제라 일단 기록은 해두려 한다.
문제에러 화면
대충 circular references 어쩌고 나오면 순환참조 이슈이다.
문제화면
문제가 발생되는 코드
코드1코드2
SecurityConfig를 빈으로 등록하려 하니 PrincipalOauth2UserService가 빈으로 사전에 등록되어야 주입이 가능하고 PrincipalOauth2UserService를 빈으로 등록하려 하니 BCryptPasswordEncoder가 미리 빈으로 등록되어서 주입을 해야 하는데 이도저도 안되니 결국 어떠한 빈도 생성하지 못한다.
해결책
순환의 고리를 끊자.
다른 Config 클래스에 빈으로 등록하기
BCryptPasswordEncoder를 상속받는 클래스를 만들고 @Component로 빈으로 등록하기
최근에 스프링부트 강의를 보는데 이전 버전 Spring Boot에서는 application.yml이 자동 생성되고 특정 버전이후부터는 application.properties 파일이 자동 생성되는 것을 확인할 수 있었다. 이 두가지 파일은 작성하는 설정의 의미는 동일하나 작성 표기법이 살짝다르다. 그리고 서로 변환이 가능해서 properties로 작성하고 싶으면 그 방식대로 작성하고 yml파일 형식으로 작성하고 싶으면 그 방식대로 작성하면 된다. 뭐가 더 편하고 좋다라는 것은 사용하는 사람들 마다 다르다. 개인적으로는 가독성은 properties 파일, 작성의 편의성은 yml파일이 좋다고 생각된다. .properties 파일은 작성법이 직관적이나 yml 파일은 초심자가 이해하는데 약간의 사전지식이 필요하다. 그리고 현재 인프런이나 패스트캠퍼스에 올라온 강의들은 yml형식으로 강의한 것들이 많으므로 이것에 대한 지식은 나름 필요하다고 생각된다. 그래서 이번 포스트에서는 application.properties파일과 application.yml파일의 차이를 알아보고 yml파일 작성법에 대해서 소개하려고 한다.
필자는 다음과 같은 프로젝트를 생성하였고 스프링부트 버전은 2.7.13이다.
프로젝트 생성
해당 버전의 스프링부트 프로젝트에서는 application.properties가 자동생성된다. 이것을 yml 파일로 바꾸고 싶다면 파일의 확장자명만 yml으로 바꾸면 된다. 이 application.properties 파일은 별다른 설정이 없다라도 기본으로 인식이 되지만 새로 생성하는 또다른 설정 파일을 인식하기 위해선 추가로 지정해주는 과정이 필요하다.
properties 파일 vs yml 파일
위의 파일은 properties 형식으로 작성한 파일이고 아래 그림은 yml 형식으로 작성한 파일이다. properties 형식은 key, value 형식으로 작성되어 사이사이에 dot(.)을 넣어서 key이름을 작성한다. 사실 이것이 계층구조인데 이러한 계층구조를 properties파일 형식에선 각 계층을 모두 적어준다.
properties
위에서 dot(.)으로 구분한 key이름은 yml 파일에선 좀 더 심플해진 계층구조로 표현이 되어 상위계층명을 생략하여 다음 라인에 입력한다. 그리고 라인마다 중간 중간에 공백이 있다. 이건 계층의 depth를 포현하며 이때 공백 한칸은 tab을 입력하기 보다는 space 두번을 쳐서 입력하는 것이 좋다. 그리고 key옆의 클론(:)과 value 사이에는 한 칸의 space가 있어야 한다.
yml
그 다음으로는 리스트를 작성할 때의 차이이다. yml 형식의 특징은 반복의 최소화이다. yml 파일의 리스트 표기는 - 로 표기를 한다. 같은 index 표기시엔 다음 라인엔 -를 쓰지 않는다. 반면에 properties 형식에서는 기존 리스트 표기형식과 동일하다.
리스트 표현형식 : properties > yml
주석
주석은 yml 파일, properties 파일 모두 #으로 동일하다.
직접 작성시
properties
최대한 인텔리센스에서 제공하는 자동완성으로 작성하는 것이 좋다. 오타를 내는 것을 조심하자.
자동완성
yml
yml 파일 자동완성은 매우 편하다. 한방에 계층 구조를 만들어준다. 그리고 한라인 한라인 엔터치고 작성하면 작성 속도도 yml 파일이 월등히 빠르다고 생각한다.
label1, label2에 값을 넣는 예제인데 기존에 표현식을 script 단에서 썼을 때 문자열 표현식을 ' '로 감싸야 했던 상황이 있었는데 th:inline="javascript"을 쓰면 문자열은 자동으로 " "를 붙여서 script단에서 동작한다. 숫자는 그냥 나온다. ( th:inline="javascript"에서 [[]]사용시한글은 퍼센트 인코딩이 되어서 나오니까, 주의할 것! )
th:inline="javascript"을 사용하고 [[ ]], [( )]을 가지고 적절하게 맞게 사용해야 한다.
[( )] 형태로 꺼내쓰면 인코딩 없이 데이터가 나온다. [[ ]] 로 꺼내쓰면 인코딩 되어 나온다.
[( )] 형태로 쓰려거든 ' '를 직접 붙여줘야 한다.
<div id="label1"></div>
<div id="label2"></div>
<script>
let name1 = '[[${name}]]';
let num1 = [[${num}]];
let names1 = '[[${names}]]';
let dto1 = '[[${dto}]]';
// 소스 보기에서 자바스크립트 배열이 아니다.
document.getElementById('label1').textContent = name1;
</script>
<script th:inline="javascript">
//let name2 = '[[${name}]]'; // 이거 쓰면 " " 붙어서 나온다.
let name2 = [[${name}]]; // 이거 쓰면 " " 붙어서 나온다.
let num2 = [[${num}]];
let names2 = [[${names}]];
let dto2 = [[${dto}]];
let dto3 = '[(${dto})]'; // ' '를 안쓰면 문자열화가 안되어서 에러난다.
// 소스 보기에서 자바스크립트 배열이다. 손댈것이 없다.
document.getElementById('label2').textContent = name2;
</script>
왜 Thymeleaf를 써야하는지는 이전 글의 jsp 설정에서 봤듯이 STS4에서 jsp 셋팅을 기본으로 지원을 하지 않는다는 점이다. 반면에 Thymeleaf는 초기 프로젝트 설정에서 Dependency로 Thymeleaf만 추가해도 셋팅이 된다. 인프런에 있는 많은 강의에서도 대부분 부트 입문자에게 Thymeleaf쪽을 공부하는 것을 추천한다. Thymeleaf는 JSP와 비슷한 것이 많은데 추가적으로 몇가지 편리한 기능들이 추가되어 JSP에서는 불편했던 것들이 Thymeleaf에서는 좀 더 작업하기 편한 것들이 있다. 무조건 Thymeleaf만을 쓰라는 것은 아니다. FreeMarker나 Groovy, Mustache 등의 템플릿 엔진도 있다. 나중에 기회가 되면 이러한 것들도 공부해보고 싶긴하다.
Thymeleaf 플러그인 설치
설치해
STS4에서 thymeleaf 관련 속성들의 인텔리센스는 위의 플러그인 설치로 인해 가능해진다. 참고하기
설치되면 이게 가능
프로젝트 셋팅
프로젝트 생성
* STS4 > New 탭 > Spring Start Project
- Name > boot-thymeleaf
- Type > Maven
- Packaging > Jar
- Java Version > 11
- Language > Java
- Group > com.test.thymeleaf
- Artifcat > boot-mybatis
- Package > com.test.thymeleaf
- Spring boot version > 2.7.13
* Dependency
- Spring Web
- Oracle Driver
- MyBatis Framework
- Lombok
- Thymeleaf
- Spring Boot DevTools
설정파일 셋팅 > application.properties
classpath이건 스프링과 동일하게 기본적으로 src/main/resources 이다.
작업할 땐 spring.thymeleaf.cache=false
# 서버 포트 번호
server.port = 8092
# JSP View Resoler, webapp, WEB-INF 폴더가 없음
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
# HikariCP settings
spring.datasource.hikari.minimumIdle=5
spring.datasource.hikari.maximumPoolSize=20
spring.datasource.hikari.idleTimeout=30000
spring.datasource.hikari.maxLifetime=2000000
spring.datasource.hikari.connectionTimeout=30000
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=hr
spring.datasource.password=java1234
#Thymeleaf
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
# 이전에 내용을 캐싱시켜준거 보여주는데 속도 빠르게 해주는데
# 개발할때는 고친게 눈에 바로 보여야 한다. 캐시 활성화 끈다.
spring.thymeleaf.cache=false
spring.thymeleaf.encoding=UTF-8
# 이설정을 하면 mybatis 다루는데 필요한 설정이 가능
mybatis.config-location=classpath:mybatis-config.xml
@Controller
public class ThymeleafController {
@GetMapping("/m1")
public void m1() {
// 요청 메소드의 반환값 > void > m1.jsp 호출
// 요청 메소드의 반환값 > void > m1.html 호출
System.out.println("m1");
}
// 위와 아래는 동일한 표현이다. > void 반환값, String 반환값
/*@GetMapping("/m1")
public String m1() {
System.out.println("m1");
return"m1"; // 이것도 마찬가지로 m1.html 호출이다.
}*/
}
★★★ Thymeleaf Standard Expression
1. Variable Expression, 변수 표현식
- ${}
- 컨트롤러 > 전달된 값 > 출력하는 역할
* 우리가 쓰던 EL과 비슷하다!
2. Selection Variable Expression, 선택 변수 표현식
- *{}
- 객체/맵 프로퍼티 > 출력
- th:object 속성과 같이 사용
3. Message Expression, 메세지 표현식
- #{}
- 스프링 메세지 > 전용 출력
4. Link URL Expression, 링크 주소 표현식
` @{}
- 링크의 URL > 전용 출력
5. Fragment Expression, 조각 표현식
- ~{}
- 조각 페이지 삽입(include 지시자 or 타일즈 > 유사 )
- 타임리프 > th:XXX 옆엔 속성이다.
"/m3" Controller 부분
기존 스프링과 동일 model에 담아서 전송, map을 사용해도 되고 int나 String으로 넣어도 model에 넣는데 차이 없음
@GetMapping("/m3")
public String m3(Model model) {
int num = mapper.getNum();
String txt = mapper.getTxt();
BoardDTO dto = mapper.getDTO();
Map<String, String> map = new HashMap<String, String>();
map.put("dog","강아지");
map.put("cat","고양이");
model.addAttribute("num", num);
model.addAttribute("txt", txt);
model.addAttribute("now", Calendar.getInstance());
model.addAttribute("dto", dto);
model.addAttribute("map", map);
return"m3";
}